Disable Umbraco AI Copilot in live cloud environment

Hi there,

Does anyone know of a way to disable the little blue AI Assistant (Copilot) popup in a live environment backoffice, when running in Umbraco Cloud on a staging and live server? I’ve looked for keys in appSettings.(Production.)json but I can’t find any relevant. I might have missed something obvious.

All the best,
Rolf

Hi @rolfis

With the help of AI, I’ve looked into this and as far as I can see there’s nothing out of the box to turn it off, no appsettings key and nothing in the AI section.

The button is added by a backoffice entry point in the Umbraco.AI.Agent.Copilot package with the alias UmbracoAIAgent.BackofficeEntryPoint.CopilotSidebar, so you can exclude it using a manifest that checks your current environment:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Infrastructure.Manifest;

public class HideCopilotManifestReader : IPackageManifestReader
{
    private readonly IHostEnvironment _env;

    public HideCopilotManifestReader(IHostEnvironment env) => _env = env;

    public Task<IEnumerable<PackageManifest>> ReadPackageManifestsAsync()
    {
        if (_env.IsDevelopment())
        {
            return Task.FromResult(Enumerable.Empty<PackageManifest>());
        }

        var manifest = new PackageManifest
        {
            Name = "Hide AI Copilot",
            AllowPublicAccess = true,
            Extensions =
            [
                new
                {
                    type = "appEntryPoint",
                    alias = "My.HideCopilot.AppEntryPoint",
                    name = "Hide AI Copilot App Entry Point",
                    js = "/App_Plugins/HideCopilot/hide-copilot.js"
                }
            ]
        };

        return Task.FromResult<IEnumerable<PackageManifest>>([manifest]);
    }
}

public class HideCopilotComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
        => builder.Services.AddSingleton<IPackageManifestReader, HideCopilotManifestReader>();
}

Then add wwwroot/App_Plugins/HideCopilot/hide-copilot.js:

export const onInit = (_host, extensionRegistry) => {
 extensionRegistry.exclude('UmbracoAIAgent.BackofficeEntryPoint.CopilotSidebar');
};

I’ve made it a public app entry point so it runs before login. That way the exclusion is in place before the Copilot registers, otherwise you can get a race where it still shows.

That code is AI generated and untested, so you will need to test it.

@mattbrailsford Any other suggestions??

Justin

If you are referring specifically to Umbraco AI Copilot, I’d say uninstall that particular package. Umbraco AI is very modular so you just install the parts you need. If you don’t want people to access the Copilot, then don’t install it.

Hi @mattbrailsford

I think they want it in dev but not once deployed on staging/live, from what I understand…

Of course, uninstalling it completely is an option if they don’t want it at all.

Justin

For clarity: we want the package in our staging environment for testing and evaluation, but not in live. Since it’s the same codebase for staging and live in Umbraco Cloud, I can’t uninstall a package in a particular environment.

And I asked Claude too, which suggested using a small decorator registered from a composer; and just drop the manifest from Umbraco.AI.Agent.Copilot in that particular environment.

(unchecked)

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Manifest;

namespace UmbracoProject.Composers;

/// <summary>
/// Shows the Umbraco AI Copilot backoffice UI only in the Staging environment.
/// </summary>
public class CopilotStagingOnlyComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        // Umbraco's own registration (added by AddBackOffice(), which runs before AddComposers()).
        ServiceDescriptor inner = builder.Services.Last(d => d.ServiceType == typeof(IPackageManifestService));
        builder.Services.Remove(inner);

        builder.Services.AddSingleton<IPackageManifestService>(sp =>
            new CopilotStagingOnlyPackageManifestService(
                CreateInner(sp, inner),
                sp.GetRequiredService<IHostEnvironment>()));
    }

    private static IPackageManifestService CreateInner(IServiceProvider sp, ServiceDescriptor d) =>
        (IPackageManifestService)(d.ImplementationInstance
            ?? d.ImplementationFactory?.Invoke(sp)
            ?? ActivatorUtilities.CreateInstance(sp, d.ImplementationType!));
}

internal sealed class CopilotStagingOnlyPackageManifestService(
    IPackageManifestService inner,
    IHostEnvironment env) : IPackageManifestService
{
    private const string CopilotPackageId = "Umbraco.AI.Agent.Copilot";

    public async Task<IEnumerable<PackageManifest>> GetAllPackageManifestsAsync() =>
        Filter(await inner.GetAllPackageManifestsAsync());

    public async Task<IEnumerable<PackageManifest>> GetPublicPackageManifestsAsync() =>
        Filter(await inner.GetPublicPackageManifestsAsync());

    public async Task<IEnumerable<PackageManifest>> GetPrivatePackageManifestsAsync() =>
        Filter(await inner.GetPrivatePackageManifestsAsync());

    // Leave the import map alone so nothing that imports @umbraco-ai/agent-copilot breaks.
    public Task<PackageManifestImportmap> GetPackageManifestImportmapAsync() =>
        inner.GetPackageManifestImportmapAsync();

    private IEnumerable<PackageManifest> Filter(IEnumerable<PackageManifest> manifests) =>
        env.IsStaging()
            ? manifests
            : manifests.Where(m => m.Id != CopilotPackageId);
}

All the best,
Rolf

Ok, I ended up with this code in CopilotStagingOnlyComposer.cs and an optional appsettings key Copilot:EnableInDevelopment, it works locally at least:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Manifest;

/// <summary>
/// Shows the Umbraco.AI.Agent.Copilot backoffice UI only in Staging and Development, never on live. The package
/// is installed everywhere (stage and live share one csproj), so its manifest is filtered out of the backoffice
/// instead. Auto-discovered via .AddComposers() in Program.cs, which runs after AddBackOffice() has registered
/// Umbraco's own IPackageManifestService.
///
/// Locally it is on by default (appsettings.Development.json). To opt out:
/// <c>dotnet user-secrets set "Copilot:EnableInDevelopment" false</c>, then restart the site.
/// The flag is honoured only in Development, so it can never enable Copilot on live.
/// </summary>
public class CopilotStagingOnlyComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        ServiceDescriptor inner = builder.Services.Last(d => d.ServiceType == typeof(IPackageManifestService));
        builder.Services.Remove(inner);

        builder.Services.AddSingleton<IPackageManifestService>(sp =>
            new CopilotStagingOnlyPackageManifestService(
                CreateInner(sp, inner),
                IsCopilotEnabled(sp.GetRequiredService<IHostEnvironment>(), builder.Config)));
    }

    private static bool IsCopilotEnabled(IHostEnvironment env, IConfiguration config) =>
        env.IsStaging() ||
        (env.IsDevelopment() && config.GetValue<bool>("Copilot:EnableInDevelopment"));

    private static IPackageManifestService CreateInner(IServiceProvider sp, ServiceDescriptor d) =>
        (IPackageManifestService)(d.ImplementationInstance
            ?? d.ImplementationFactory?.Invoke(sp)
            ?? ActivatorUtilities.CreateInstance(sp, d.ImplementationType!));
}

internal sealed class CopilotStagingOnlyPackageManifestService(
    IPackageManifestService inner,
    bool copilotEnabled) : IPackageManifestService
{
    private const string CopilotPackageId = "Umbraco.AI.Agent.Copilot";

    public async Task<IEnumerable<PackageManifest>> GetAllPackageManifestsAsync() =>
        Filter(await inner.GetAllPackageManifestsAsync());

    public async Task<IEnumerable<PackageManifest>> GetPublicPackageManifestsAsync() =>
        Filter(await inner.GetPublicPackageManifestsAsync());

    public async Task<IEnumerable<PackageManifest>> GetPrivatePackageManifestsAsync() =>
        Filter(await inner.GetPrivatePackageManifestsAsync());

    // Left unfiltered so nothing importing @umbraco-ai/agent-copilot fails to resolve.
    public Task<PackageManifestImportmap> GetPackageManifestImportmapAsync() =>
        inner.GetPackageManifestImportmapAsync();

    private IEnumerable<PackageManifest> Filter(IEnumerable<PackageManifest> manifests) =>
        copilotEnabled
            ? manifests
            : manifests.Where(m => m.Id != CopilotPackageId);
}

All the best,
Rolf

just a thought… though depends if you are running staging in debug vs release…

<ItemGroup>
  <PackageReference Include="Umbraco.AI.Agent.Copilot" Condition="'$(Configuration)' == 'Debug'" />
</ItemGroup>

or if you are passing in the environment to the build process..

dotnet publish "%DEPLOYMENT_SOURCE%\Project\Project.Web.csproj" -c Release -r win-x64 -f net10.0 -p:PublishSingleFile=false -p:EnvironmentName=%ASPNETCORE_ENVIRONMENT%
<ItemGroup>
  <!-- Exclude the package if the environment is Production -->
  <PackageReference Include="Umbraco.AI.Agent.Copilot" Condition=" '$(EnvironmentName)' != 'Production'" />
</ItemGroup>

@mistyn8 I knew there had to be a simpler solution that I’d forgot about… thanks Mike! :smiley:

All the best,
Rolf