Portal Community

Complete Custom Guard Example

// DomainAllowlistGuard.cs — only allows execution if the email domain is approved
using BizFirst.Ai.GuardRails.Domain.Contracts;
using BizFirst.Ai.GuardRails.Domain.Enums;
using BizFirst.Ai.GuardRails.Domain.Models;

public class DomainAllowlistGuard : IGuardRail
{
    // IGuardRailExecutor
    public string Name    => "DomainAllowlistGuard";
    public string Version => "1.0.0";
    public IReadOnlyList<GuardRailPhase> SupportedPhases =>
        new[] { GuardRailPhase.Pre };

    // IGuardRailDescriptor
    public string GuardName    => "Domain Allowlist Guard";
    public string Description  => "Blocks execution if the recipient email domain is not in the allowlist.";
    public bool IsSecurityCritical => true;

    public string ConfigurationSchema => @"{
      ""type"": ""object"",
      ""properties"": {
        ""allowedDomains"": {
          ""type"": ""array"",
          ""items"": { ""type"": ""string"" },
          ""description"": ""List of approved email domains (e.g. ['example.com'])""
        }
      },
      ""required"": [""allowedDomains""]
    }";

    public IReadOnlyList<GuardRailDependency> Dependencies =>
        Array.Empty<GuardRailDependency>();

    private IList<string> _allowedDomains = new List<string>();

    public void SetConfiguration(IDictionary<string, object?> configuration)
    {
        if (configuration.TryGetValue("allowedDomains", out var domains)
            && domains is IList<object> list)
        {
            _allowedDomains = list.Select(d => d?.ToString() ?? "").ToList();
        }
    }

    // IGuardRailExecutor.ExecuteAsync
    public Task<GuardRailCheckResult> ExecuteAsync(
        GuardRailExecutionContext context,
        GuardRailPhase phase,
        CancellationToken ct)
    {
        if (phase != GuardRailPhase.Pre)
            return Task.FromResult(GuardRailCheckResult.Success());

        var input = context.Input as IDictionary<string, object>;
        var email = input?.TryGetValue("recipientEmail", out var e) == true
            ? e?.ToString() ?? "" : "";

        var domain = email.Contains('@')
            ? email.Split('@')[1].ToLowerInvariant() : "";

        if (!_allowedDomains.Contains(domain))
        {
            return Task.FromResult(GuardRailCheckResult.Blocked(
                $"Email domain '{domain}' is not in the allowlist.",
                metadata: new Dictionary<string, object?> { { "domain", domain } }));
        }
        return Task.FromResult(GuardRailCheckResult.Success());
    }

    // IGuardRailConfigValidator
    public bool Validate(IDictionary<string, object?> configuration)
        => ValidateWithDetails(configuration).IsValid;

    public GuardRailConfigValidationResult ValidateWithDetails(IDictionary<string, object?> configuration)
    {
        if (!configuration.ContainsKey("allowedDomains"))
            return GuardRailConfigValidationResult.Invalid(new[] { "allowedDomains is required" });
        return GuardRailConfigValidationResult.Valid();
    }
}

Registering the Custom Guard

// In your INodeExecutorDependency.RegisterDefaults() or startup
services.AddScoped<IGuardRail, DomainAllowlistGuard>();
// The IGuardRailsExecutor resolves all IGuardRail registrations automatically

Testing Pattern

[Fact]
public async Task Guard_BlocksDisallowedDomain()
{
    var guard = new DomainAllowlistGuard();
    guard.SetConfiguration(new Dictionary<string, object?>
    {
        { "allowedDomains", new List<object> { "example.com" } }
    });

    var ctx = new GuardRailExecutionContext
    {
        Input = new Dictionary<string, object>
            { { "recipientEmail", "alice@badactor.io" } }
    };

    var result = await guard.ExecuteAsync(ctx, GuardRailPhase.Pre);

    Assert.False(result.IsAllowed);
    Assert.Contains("badactor.io", result.ErrorMessage);
}
SetConfiguration Is Called Before ExecuteAsync The guard infrastructure calls SetConfiguration() with the Atlas Form values before the pipeline runs. Store configuration in instance fields after parsing. Do not read configuration inside ExecuteAsync.