Skip to main content

Safety Guard

Capabilities

Safety Guard

The EnerOS Safety Guard is the last line of defense before an Agent decision takes effect, located on the command execution channel in kernel space. All control commands from Agents, APIs, and external systems must pass through the safety gateway’s interception and validation, ensuring that even if upper-layer logic has defects, it will not cause power system accidents.

The power system is a cyber-physical system. A single wrong command can cause large-scale blackouts, equipment damage, or even personal injury. EnerOS’s Safety Guard builds multiple layers of defense based on the “defense in depth” principle. A single point of failure will not allow an accident to penetrate to the physical grid. This is EnerOS’s security moat that distinguishes it from a general-purpose AgentOS.

Architecture Overview

┌─────────────────────────────────────────────────────┐
│  Agent / API / External System                      │
└──────────────────┬──────────────────────────────────┘
                   │ submit(cmd)
┌──────────────────┴──────────────────────────────────┐
│  SafetyGateway (Chain of Responsibility)            │
│  ┌──────────────────────────────────────────────┐   │
│  │ 1. AuthStage      mTLS / OAuth2 / mTLS+Token │   │
│  │ 2. PermissionStage RBAC / ABAC                │   │
│  │ 3. ConstraintStage Strict constraint validation│   │
│  │ 4. RuleStage      Power safety rules          │   │
│  │ 5. RateLimitStage Rate and quota              │   │
│  │ 6. AuditStage     WORM audit                  │   │
│  └──────────────────────────────────────────────┘   │
│                ↓ Verdict                            │
│         Allow / Deny / Project                      │
└──────────────────┬──────────────────────────────────┘
                   │ execute
┌──────────────────┴──────────────────────────────────┐
│  Equipment Execution Layer (Real-time Domain)       │
└─────────────────────────────────────────────────────┘

Safety Gateway Architecture

The gateway uses the chain of responsibility pattern. Each command passes through in order: identity authentication → permission validation → constraint validation → safety rules → rate limiting → audit recording.

use eneros_safety::{SafetyGateway, Pipeline};
use std::time::Duration;

let gateway = SafetyGateway::builder()
    .stage(AuthStage::mtls())
    .stage(PermissionStage::rbac())
    .stage(ConstraintStage::strict())
    .stage(RuleStage::default())
    .stage(RateLimitStage::new(100, Duration::from_secs(1)))
    .stage(AuditStage::worm())
    .build();

Chain of Responsibility Stages

StageResponsibilityFailure ActionTypical Cost
AuthStageIdentity authentication (mTLS / Token)Deny< 50μs
PermissionStagePermission validation (RBAC / ABAC)Deny< 20μs
ConstraintStagePhysical constraint validationDeny / Project< 200μs
RuleStagePower safety rules (five-interlock, etc.)Deny / Project< 30μs
RateLimitStageRate and quotaDeny< 5μs
AuditStageWORM audit writeNon-blocking< 50μs
Total--< 100μs

Command Interception

Each command enters the gateway via submit and returns Allow / Deny / Project:

use eneros_safety::{Command, Verdict, BreakerId};

let cmd = Command::OpenBreaker { id: breaker_id };

match gateway.submit(cmd).await? {
    Verdict::Allow => {
        ctx.execute(cmd).await?;
    }
    Verdict::Deny(reason) => {
        log::warn!("Command intercepted: {}", reason);
        ctx.notify_agent("Command rejected by safety gateway").await?;
    }
    Verdict::Project(alternative) => {
        log::info!("Command projected to feasible solution: {:?}", alternative);
        ctx.execute(alternative).await?;
    }
}

Command Variants

VariantMeaningRisk Level
OpenBreaker { id }Open a circuit breakerHigh
CloseBreaker { id }Close a circuit breakerHigh
OpenDisconnector { id }Open a disconnectorHigh
CloseDisconnector { id }Close a disconnectorMedium
SetGenerator { id, mw }Adjust unit outputMedium
AdjustTap { id, tap }Adjust tap changerLow
SetLoad { id, mw }Adjust loadMedium
GroundDevice { id }Install groundingHigh
EmergencyStopEmergency stopCritical

Verdict Variants

VariantMeaningHandling
AllowAll passedExecute
Deny(reason)RejectedNotify Agent
Project(alt)Projected to a feasible solutionExecute alt
Delay(duration)Delayed executionRetry after waiting
ManualReviewManual review requiredEscalate to human

Safety Rules

The rule system supports composable policies, covering typical power safety scenarios:

use eneros_safety::{Rule, RuleSet};

let rules = RuleSet::new()
    // Prohibit disconnecting a disconnector under load
    .add(Rule::no_load_break_disconnector())
    // Synchronization check
    .add(Rule::sync_check_before_close())
    // Five-interlock locking
    .add(Rule::five_interlocks())
    // Maintenance zone isolation
    .add(Rule::maintenance_zone_isolation())
    // Frequency anomaly blocking
    .add(Rule::frequency_block(49.0, 51.0))
    // Voltage limit blocking
    .add(Rule::voltage_block(0.85, 1.15))
    // Custom rule
    .add(Rule::custom("no_dispatch_during_maintenance", |cmd, ctx| {
        if ctx.maintenance_in_progress() && cmd.is_dispatch() {
            Verdict::Deny("Dispatch operations are prohibited during maintenance".into())
        } else {
            Verdict::Allow
        }
    }));

Built-in Rule Overview

RuleCheck ContentApplicable Equipment
no_load_break_disconnectorProhibit disconnecting a disconnector under loadDisconnector
sync_check_before_closeSynchronization check before closingCircuit breaker
five_interlocksFull five-interlock checkAll equipment
maintenance_zone_isolationMaintenance zone isolationAll equipment
frequency_blockFrequency limit blockingSystem-level
voltage_blockVoltage limit blockingBus-level
overload_blockEquipment overload blockingLine, transformer
cold_load_pickupCold load pickup limitLoad switch
reclosing_blockReclosing blockingCircuit breaker
parallel_protectionParallel protectionGenerator

Operation Ticket

For complex operation sequences, the operation ticket process is mandatory to ensure order and isolation:

use eneros_safety::{OperationTicket, Step};

let ticket = OperationTicket::new()
    .step(Step::open(bus_id_1))
    .step(Step::verify_open(bus_id_1))
    .step(Step::ground(bus_id_1))
    .step(Step::maintain(bus_id_1))
    .step(Step::unground(bus_id_1))
    .step(Step::close(bus_id_1))
    .step(Step::verify_closed(bus_id_1));

let ticket_id = gateway.submit_ticket(ticket).await?;
// Must be executed in order; skipping a step will be rejected
// Each step is automatically recorded to WORM audit after execution

// Query operation ticket status
let status = gateway.ticket_status(ticket_id).await?;
println!("Current step: {}/{}", status.current_step, status.total_steps);

OperationTicket Fields

FieldTypeDescription
idTicketIdUnique operation ticket ID
stepsVecOperation step sequence
created_byAgentIdCreator
approved_byOptionApprover (for critical operations)
statusTicketStatusDraft / Pending / Active / Done / Cancelled
timeoutDurationAuto-cancel timeout

Rate and Quota

Prevents “command storms” caused by Agent runaway:

// Per-Agent rate limit
gateway.set_rate_limit("dispatch_agent", 100, Duration::from_secs(1));

// Critical device quota
gateway.set_quota("circuit_breaker_5", 5, Duration::from_hours(1));

// Burst bucket
gateway.set_burst_limit("protection_agent", 10, 100, Duration::from_secs(1));

// Global rate
gateway.set_global_rate_limit(10000, Duration::from_secs(1));

Rate Strategies

StrategyMeaningApplicable Scenario
TokenBucketToken bucketGeneral Agent
LeakyBucketLeaky bucketSmooth traffic
SlidingWindowSliding windowPrecise rate limiting
QuotaTotal quotaCritical device

Audit and WORM

All command decisions (including intercepted ones) are written to WORM (Write Once Read Many) storage, meeting NERC CIP and IEC 62443 forensics requirements:

use eneros_safety::{AuditQuery, Range, VerdictFilter};

let audit_log = gateway.audit_log();

// Query audit
let entries = audit_log.query(
    AuditQuery::new()
        .agent("dispatch_agent")
        .range(Range::last(Duration::from_hours(24)))
        .verdict(VerdictFilter::All)
).await?;

for entry in &entries {
    println!("{} | {:?} | {} | {:?}",
        entry.timestamp, entry.verdict, entry.command, entry.agent);
}

// Export audit report (compliance)
let report = audit_log.export_report(
    Range::between(month_start, month_end),
    Format::Csv,
).await?;
std::fs::write("audit_202607.csv", report)?;

AuditEntry Fields

FieldTypeDescription
timestampTimestampCommand submission time
agentAgentIdSubmitter
commandCommandCommand content
verdictVerdictDecision result
reasonOptionReason for rejection
duration_usu64Validation cost (μs)
tenantTenantIdTenant
signature[u8; 32]Tamper-proof signature

Emergency Stop

Supports a hardware-level emergency stop button that bypasses all Agents and disconnects directly:

use eneros_safety::EmergencyScope;

gateway.emergency_stop(EmergencyScope::All).await?;
// The kernel automatically:
//   1. Pauses all Agents
//   2. Zeros all control outputs
//   3. Enters a safe state
//   4. Notifies operations staff
//   5. Starts the emergency process

// Localized emergency stop
gateway.emergency_stop(EmergencyScope::Region("substation_a")).await?;

// Resume operation (requires manual confirmation)
gateway.resume_from_emergency().await?;

EmergencyScope Variants

VariantScopeRecovery Method
AllWhole systemManual
Region(name)Specified regionManual
Agent(id)Single AgentAutomatic
Device(id)Single deviceAutomatic

Safety Level Matrix

LevelDescriptionStagesApplicable Scenario
L0Fully openAudit onlyLab testing
L1Constraint validationAuth + ConstraintDigital twin
L2Constraint + rules+ Rule + RateLimitProduction environment
L3Full chain + operation ticket+ Permission + TicketCritical infrastructure
L4L3 + independent hardware validation+ Hardware Safety ModuleNational grid
use eneros_safety::SafetyLevel;

// Configure safety level
gateway.set_level(SafetyLevel::L3)?;

// Dynamic switch at runtime (can only escalate to stricter)
gateway.escalate_to(SafetyLevel::L4).await?;

Performance Metrics

OperationLatencyThroughput
Single command validation (full chain)< 100μs10k cmd/s
AuthStage< 50μs-
PermissionStage< 20μs-
ConstraintStage (1000 nodes)< 200μs-
RuleStage< 30μs-
Operation ticket execution (5 steps)< 1ms-
WORM audit write< 50μs100k entries/s
Emergency stop response< 1ms-
Audit query (24h)< 50ms-

Test environment: 4 cores / 8GB / Ubuntu 22.04.

Configuration Parameters

Safety-related configuration in eneros.toml:

[safety]
# Safety level: L0 / L1 / L2 / L3 / L4
level = "L3"
# Whether to enforce operation tickets
require_ticket = true
# Default rate limit (cmd/s)
default_rate_limit = 100
# Critical device quota (cmd/h)
critical_device_quota = 5
# WORM audit storage path
audit_dir = "/var/eneros/audit"
# Audit retention period (days)
audit_retention_days = 3650
# Whether to enable the emergency stop hardware interface
hardware_emergency_stop = true
# Emergency stop heartbeat timeout (ms)
emergency_heartbeat_ms = 100
ParameterTypeDefaultDescription
levelenumL3Safety level
require_ticketbooltrueEnforce operation tickets
default_rate_limitu32100Default rate limit
critical_device_quotau325Critical device quota
audit_dirString/var/eneros/auditWORM audit path
audit_retention_daysu323650Audit retention period
hardware_emergency_stopbooltrueHardware emergency stop
emergency_heartbeat_msu32100Heartbeat timeout

Relationship with Other Capabilities

Related CapabilityInteraction
Physical Constraint DecisionThe constraint engine is the core of the gateway
Zero Trust and Security EnhancementIdentity authentication and audit
Multi-Agent CollaborationSource of Agent commands
Real-Time Dual Execution DomainReal-time domain commands also pass through the gateway
Multi-tenant and IsolationCommands are audited per tenant namespace
Time-Series Native OperationsAudit logs are ingested
Grid Topology First-Class CitizenOperation tickets depend on topology validation