Portal Community

Four-Layer Architecture

GuardRails is organized into four layers, each with a clear responsibility. Higher layers depend on lower ones; lower layers have zero knowledge of higher layers.

LayerProjectResponsibility
Domain BizFirst.Ai.GuardRails.Domain Pure contracts: interfaces, models, enums. Zero external dependencies. All other layers reference this.
Service BizFirst.Ai.GuardRails.Service Orchestration, config resolution, circuit breaker, guard registry, logging. Infrastructure only — no guard business logic.
Providers Provider.Core, Provider.PII Guard implementations. Each guard is a focused class with a single security or reliability concern.
Execution BizFirst.Ai.GuardRails.Execution Public facade consumed by BaseNodeExecutor. Coordinates Pre/Post/Error handlers.

The Execution Context

A single GuardRailExecutionContext instance is created at the start of each node execution and flows through all phases. Guards read from it, write to it, and use its Metadata dictionary to pass state between phases.

public class GuardRailExecutionContext
{
    public long TenantId { get; set; }       // multi-tenant isolation
    public long UserId { get; set; }          // actor performing the operation
    public string OperationId { get; set; }   // unique per node execution
    public object? Input { get; set; }        // node input data
    public object? Output { get; set; }       // set after node runs; guards may modify
    public IDictionary<string, object?> Metadata { get; set; }  // shared state across all phases
    public string TraceId { get; set; }       // distributed trace correlation
    public string PhaseId { get; set; }       // current phase name ("Pre" / "Post" / "Error")
    public DateTime CreatedAt { get; set; }
}
Metadata is the cross-phase communication channel TimeoutGuard stores context.Metadata["__timeout_start"] in Pre, then reads it in Post to compute elapsed time. CostTrackingGuard stores a reservation ID in Pre, confirms it in Post. This pattern keeps guards stateless at the instance level.

Execution Flow

BaseNodeExecutor.ExecuteAsync()

Entry point. Creates a GuardRailExecutionContext with TenantId, UserId, OperationId, Input from the node execution context.

1

Pre — IGuardRailsExecutor.ExecutePreAsync(context)

Runs all configured Pre-phase guards sequentially. Each guard calls ExecuteAsync(context, GuardRailPhase.Pre). If any returns IsAllowed=false, execution stops — the node never runs.

N

ExecuteInternalAsync() — node business logic runs

The actual node: HTTP call, email send, database query, AI model call. If it throws, go to Error phase.

2

Post — IGuardRailsExecutor.ExecutePostAsync(context, output)

Sets context.Output = output. Runs Post-phase guards. If OutputModified=true, the node's output data is replaced with the modified value (e.g., PII redacted).

!

Error — IGuardRailsExecutor.ExecuteOnErrorAsync(context, exception)

Only runs if the node threw. Records circuit breaker state, writes audit events. Always returns IsAllowed=true — error handlers never block.

Guard Result: GuardRailCheckResult

Every guard returns a GuardRailCheckResult. Three factory methods cover all outcomes:

ResultIsAllowedMeaning
Success(outputModified, metadata)trueGuard passed. outputModified=true signals the caller to update output data from context.Output.
Blocked(message, retryAfter, metadata)falseGuard blocked. Execution stops. retryAfterSeconds is optional retry hint.
Warning(message, metadata)trueGuard noticed something but allows. ErrorMessage is set but IsAllowed=true. Used for non-critical violations.

Aggregated Result: GuardRailsExecutionResult

The public facade returns GuardRailsExecutionResult which aggregates all individual guard results from a phase:

public class GuardRailsExecutionResult
{
    public bool IsAllowed { get; set; }                   // false if any guard blocked
    public IList<string> ExecutedGuards { get; set; }    // names of all guards that ran
    public IList<GuardRailViolation> Violations { get; set; }  // all block reasons
    public GuardRailsExecutionTimeline Timeline { get; set; }  // per-guard durations
    public long TotalDurationMs { get; set; }
}

The IsSecurityCritical Flag

Each guard declares IsSecurityCritical. This controls behavior when the guard's own circuit breaker opens (i.e., the guard infrastructure itself fails):

IsSecurityCriticalCircuit Open BehaviorExamples
true Fail-Secure Block execution. Unknown state = deny. InputValidationGuard, RateLimitingGuard, PiiDetectionGuard, PiiRedactionGuard
false Fail-Open Allow execution with a warning log. TimeoutGuard, CircuitBreakerGuard

Guard Interface

Every guard implements IGuardRail, a composite of three focused interfaces:

// Core execution
public interface IGuardRailExecutor
{
    string Name { get; }                                       // e.g. "TimeoutGuard"
    string Version { get; }                                    // e.g. "1.0.0"
    IReadOnlyList<GuardRailPhase> SupportedPhases { get; }    // which phases this guard handles
    bool IsSecurityCritical { get; }
    Task<GuardRailCheckResult> ExecuteAsync(
        GuardRailExecutionContext context,
        GuardRailPhase phase,
        CancellationToken cancellationToken = default);
}

// Config validation
public interface IGuardRailConfigValidator
{
    bool Validate(IDictionary<string, object?> configuration);
    GuardRailConfigValidationResult ValidateWithDetails(IDictionary<string, object?> configuration);
}

// Discovery / metadata
public interface IGuardRailDescriptor
{
    string GuardName { get; }
    string Description { get; }
    string ConfigurationSchema { get; }  // JSON Schema as string
    IReadOnlyList<GuardRailDependency> Dependencies { get; }
}

// Composite — every guard implements all three
public interface IGuardRail : IGuardRailExecutor, IGuardRailConfigValidator, IGuardRailDescriptor { }

SetConfiguration Pattern

Guards are stateless at construction time. Configuration is injected at resolution time via:

public void SetConfiguration(IDictionary<string, object?> configuration)
{
    _configuration = configuration ?? new Dictionary<string, object?>();
}

This is called by GuardConfigResolver or GuardFactory before the guard's ExecuteAsync is called. Guards read their settings from _configuration during execution, never during construction.