Portal Community

The Guard Contract

Every guard implements IGuardRail, a composite of three focused interfaces. Implement all three and your guard is a first-class citizen in the GuardRails pipeline.

// A guard must implement all three:
public interface IGuardRail : IGuardRailExecutor, IGuardRailConfigValidator, IGuardRailDescriptor { }

// IGuardRailExecutor: the execution contract
Task<GuardRailCheckResult> ExecuteAsync(
    GuardRailExecutionContext context,
    GuardRailPhase phase,
    CancellationToken cancellationToken = default);

// IGuardRailConfigValidator: validate configuration at load time
GuardRailConfigValidationResult ValidateWithDetails(IDictionary<string, object?> configuration);

// IGuardRailDescriptor: metadata for discovery and tooling
string GuardName { get; }
string Description { get; }
string ConfigurationSchema { get; }  // JSON Schema as string
IReadOnlyList<GuardRailDependency> Dependencies { get; }

Step-by-Step: Building a Custom Guard

Example: a ContentModerationGuard that calls a moderation API to check if AI output is appropriate.

1

Implement the guard class

namespace MyCompany.Guards;

public class ContentModerationGuard : IGuardRail
{
    private IDictionary<string, object?> _configuration = new Dictionary<string, object?>();
    private readonly IContentModerationClient _client;

    public ContentModerationGuard(IContentModerationClient client)
    {
        _client = client;
    }

    // IGuardRailExecutor
    public string Name => "ContentModerationGuard";
    public string Version => "1.0.0";
    public IReadOnlyList<GuardRailPhase> SupportedPhases =>
        new[] { GuardRailPhase.Post };  // Post: check AI output
    public bool IsSecurityCritical => true;  // fail-secure

    public async Task<GuardRailCheckResult> ExecuteAsync(
        GuardRailExecutionContext context,
        GuardRailPhase phase,
        CancellationToken cancellationToken = default)
    {
        if (phase != GuardRailPhase.Post || context.Output == null)
            return GuardRailCheckResult.Success();

        var outputText = context.Output.ToString() ?? string.Empty;
        var threshold = GetConfigDouble("threshold", 0.8);

        var result = await _client.CheckContentAsync(outputText, cancellationToken);

        if (result.HarmScore > threshold)
        {
            return GuardRailCheckResult.Blocked(
                $"Content moderation failed: harm score {result.HarmScore:F2} exceeds threshold {threshold:F2}",
                metadata: new Dictionary<string, object?>
                {
                    ["harm_score"] = result.HarmScore,
                    ["threshold"] = threshold,
                    ["categories"] = result.HarmCategories
                });
        }

        return GuardRailCheckResult.Success();
    }

    // SetConfiguration — called by GuardConfigResolver before ExecuteAsync
    public void SetConfiguration(IDictionary<string, object?> configuration)
    {
        _configuration = configuration ?? new Dictionary<string, object?>();
    }

    private double GetConfigDouble(string key, double defaultValue) =>
        _configuration.TryGetValue(key, out var v) && v is double d ? d : defaultValue;

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

    public GuardRailConfigValidationResult ValidateWithDetails(
        IDictionary<string, object?> configuration)
    {
        var errors = new List<string>();
        if (!configuration.ContainsKey("threshold"))
            errors.Add("'threshold' is required");
        return errors.Any()
            ? GuardRailConfigValidationResult.Invalid(errors)
            : GuardRailConfigValidationResult.Valid();
    }

    // IGuardRailDescriptor
    public string GuardName => Name;
    public string Description => "Calls content moderation API to check AI output safety.";
    public string ConfigurationSchema => @"{
        ""type"": ""object"",
        ""required"": [""threshold""],
        ""properties"": {
            ""threshold"": { ""type"": ""number"", ""minimum"": 0, ""maximum"": 1 }
        }
    }";
    public IReadOnlyList<GuardRailDependency> Dependencies => new[]
    {
        new GuardRailDependency
        {
            ServiceName = "ContentModerationApi",
            Description = "External content moderation service",
            IsRequired = true
        }
    };
}
2

Register with DI

// Option A: Register directly via DI
services.AddSingleton<IContentModerationClient, AzureContentModerationClient>();
services.AddSingleton<ContentModerationGuard>();

// Option B: Register as a named guard via GuardRegistry (for dynamic discovery)
services.AddSingleton<IGuardRail, ContentModerationGuard>();
3

Register with GuardRegistry at startup

// In your application startup / hosted service:
var registry = serviceProvider.GetRequiredService<IGuardRegistry>();
registry.Register("ContentModerationGuard", new GuardDefinition
{
    Id = "ContentModerationGuard",
    Type = "content-safety"
});
4

Use in node configuration

{
  "guardRails": {
    "individual": [
      {
        "name": "ContentModerationGuard",
        "enabled": true,
        "config": {
          "threshold": 0.75
        }
      }
    ]
  }
}

Plugin Assembly Loading

For guards that live in external assemblies (shipped as DLLs), use the plugin loader:

// GuardRegistry.LoadPluginsAsync() delegates to IGuardPluginLoader
// which scans configured plugin paths, loads assemblies, and registers GuardDefinitions

// The plugin is then accessible by name in any node config:
{ "name": "ContentModerationGuard", "config": { "threshold": 0.8 } }
Plugin assemblies must be signed IPluginSignatureValidator validates assembly signatures before loading. Unsigned plugins are rejected. See Plugin Security for the signing requirements.

Guard Design Principles