Portal Community

Integration Architecture

The ProcessEngine uses a layered executor hierarchy. GuardRails hooks into BaseNodeExecutor — the common base class for all node executors — so every node type participates in the guard pipeline without any additional work:

LayerClassGuardRails Role
Base BaseNodeExecutor Owns the guard hook sequence: Pre → Execute → Post. Calls INodeGuardExecutor.
Domain Contract INodeGuardExecutor The interface BaseNodeExecutor depends on. Three methods: ExecutePreAsync, ExecutePostAsync, ExecuteOnErrorAsync.
Implementation NodeGuardExecutor (Service layer) Resolves config, instantiates guards, runs pipeline, returns GuardRailsExecutionResult.
Fallback IRateLimitingOrchestrator Optional complementary tier for rate limiting; pluggable alongside or instead of GuardRails when INodeGuardExecutor is not configured.

The Guard Hook Sequence

BaseNodeExecutor is organized as a partial class. The guard hooks are in the guard-specific partial file and called from the main execution flow:

1

OnPreGuardRails(context)

Constructs GuardRailExecutionContext from the node's NodeExecutionContext. Calls INodeGuardExecutor.ExecutePreAsync(guardContext). If IsAllowed=false, raises GuardRailBlockedException — the node never runs.

2

Node executes

The node's own ExecuteAsync() runs normally. The guard context is held in scope — Metadata persists across phases.

3

OnPostGuardRails(context, nodeResult)

Calls INodeGuardExecutor.ExecutePostAsync(guardContext, nodeResult.OutputData). If OutputModified=true, replaces nodeResult.OutputData with the guard-modified output. If IsAllowed=false, raises exception.

E

OnErrorGuardRails(context, exception) (on exception)

Called from the catch block when the node throws. Calls ExecuteOnErrorAsync. Error-phase guards observe and log — they never block execution. The original exception propagates normally.

Context Construction

GuardRailExecutionContext is built from the node's NodeExecutionContext at the start of OnPreGuardRails:

var guardContext = new GuardRailExecutionContext
{
    TenantId    = context.TenantId,
    UserId      = context.UserId,
    NodeId      = context.NodeId,
    NodeType    = context.NodeType,
    ExecutionId = context.ExecutionId,
    TraceId     = context.TraceId,
    SourceIp    = context.SourceIp,
    Input       = context.InputData,
    Output      = null,        // populated after node runs, before Post phase
    Metadata    = new Dictionary<string, object?>()  // persists across phases
};
Metadata persists across phases The same guardContext instance is passed to Pre, Post, and Error phases. Guards that store state in context.Metadata (e.g., __timeout_start in TimeoutGuard) can read it back in later phases. This enables cross-phase correlation without shared mutable fields on the guard class.

OutputModified Propagation

When a Post-phase guard modifies the output (e.g., PiiRedactionGuard scrubs an email address), it sets OutputModified=true in the GuardRailCheckResult. BaseNodeExecutor checks this and replaces the node's output:

// In BaseNodeExecutor.OnPostGuardRails():
var postResult = await _guardExecutor.ExecutePostAsync(guardContext, nodeResult.OutputData);

if (postResult.OutputModified)
{
    // Replace node output with the guard-modified version
    nodeResult.OutputData = guardContext.Output;
}

if (!postResult.IsAllowed)
{
    throw new GuardRailBlockedException(postResult);
}

The downstream node and execution log both receive the redacted/modified version. The original node output never escapes the execution boundary.

Error Phase: Observe-Only

Error-phase guards run when the node throws an exception. They are strictly observational — they can log, update metrics, or enrich the exception with context, but they cannot block execution or swallow the exception:

// In BaseNodeExecutor, catch block:
catch (Exception ex)
{
    await OnErrorGuardRails(guardContext, ex);
    throw;  // original exception always re-thrown
}

This guarantees that guard failures in the error phase don't create double-exception scenarios or swallow legitimate errors.

Guard Configuration Resolution

NodeGuardExecutor resolves the guard configuration for each node execution. The resolution chain:

// NodeGuardExecutor calls GuardConfigResolver for each node:
var config = await _configResolver.ResolveAsync(nodeId, tenantId);
// → checks cache (15-min TTL)
// → falls back to configured guard templates
// → returns null if no guards configured (no-op)

if (config == null) return GuardRailsExecutionResult.Empty();

Violation Audit Trail

Every time a guard returns IsAllowed=false, GuardRailsAuditLogger writes an audit event asynchronously. The audit logger batches writes so violation recording never adds latency to the execution hot path:

// GuardRailViolationAuditEvent (no Input/Output — allowlist approach)
{
    OccurredAtUtc: "2026-05-25T14:32:01Z",
    TenantId:      42,
    UserId:        1001,
    GuardName:     "PiiDetectionGuard",
    Phase:         0,  // Pre
    OperationId:   "op-abc-123",
    Reason:        "PII detected: Email, SSN",
    TraceId:       "trace-xyz-789"
}

Execution Timeline in Results

GuardRailsExecutionResult carries a GuardRailsExecutionTimeline showing guard-by-guard forensics. This is returned to the caller and is available for debugging and compliance reporting:

// Sample timeline from execution result:
Guard Timeline:
  TimeoutGuard       (pre,  2ms)   ✓
  RateLimitingGuard  (pre,  5ms)   ✓
  PiiDetectionGuard  (pre, 12ms)   ✗  [BLOCKED: SSN, Email detected]

What Happens When No Guards Are Configured

If GuardConfigResolver finds no configuration for a node, NodeGuardExecutor returns GuardRailsExecutionResult.Empty(). BaseNodeExecutor treats this as IsAllowed=true — the node runs without any guard overhead. Nodes without guard configuration are zero-cost.

Future: Octopus / Phase 6 Plan

The current integration is at the BaseNodeExecutor level (individual nodes). A planned Phase 6 extension will add guard execution at the ProcessElementExecutor level, enabling guards that span entire process steps rather than individual nodes. This will support workflow-level rate limiting and cross-node PII detection without duplicating per-node configuration.

Phase 6 is planned — not yet implemented ProcessElementExecutor-level guards were reverted from the Phase 5 implementation. Current guard execution is exclusively at BaseNodeExecutor level.