Plugins & Custom
Bring Your Own Guard
GuardRails is fully extensible. Any security or reliability concern that the built-in guards don't cover can be implemented as a custom guard and registered as a plugin — participating in the same pipeline, configuration system, and audit trail.
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
- Stateless at construction: Never store execution state in instance fields. Use
context.Metadatafor cross-phase state. - Only handle your phase: Check
phase == GuardRailPhase.Pre(or Post/Error) at the start ofExecuteAsync. ReturnSuccess()for phases you don't handle. - Declare
IsSecurityCriticalhonestly: If your guard's failure leaves a security gap, settrue. If it's an optimization guard that can safely be skipped, setfalse. - Validate config at load time: Implement
ValidateWithDetailsto catch bad configuration at startup — not at runtime during a node execution. - Declare dependencies: If your guard needs Redis, a moderation API, or another service, declare it in
Dependencies. This enables health checking and documentation generation.