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
| Stage | Responsibility | Failure Action | Typical Cost |
|---|
| AuthStage | Identity authentication (mTLS / Token) | Deny | < 50μs |
| PermissionStage | Permission validation (RBAC / ABAC) | Deny | < 20μs |
| ConstraintStage | Physical constraint validation | Deny / Project | < 200μs |
| RuleStage | Power safety rules (five-interlock, etc.) | Deny / Project | < 30μs |
| RateLimitStage | Rate and quota | Deny | < 5μs |
| AuditStage | WORM audit write | Non-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
| Variant | Meaning | Risk Level |
|---|
| OpenBreaker { id } | Open a circuit breaker | High |
| CloseBreaker { id } | Close a circuit breaker | High |
| OpenDisconnector { id } | Open a disconnector | High |
| CloseDisconnector { id } | Close a disconnector | Medium |
| SetGenerator { id, mw } | Adjust unit output | Medium |
| AdjustTap { id, tap } | Adjust tap changer | Low |
| SetLoad { id, mw } | Adjust load | Medium |
| GroundDevice { id } | Install grounding | High |
| EmergencyStop | Emergency stop | Critical |
Verdict Variants
| Variant | Meaning | Handling |
|---|
| Allow | All passed | Execute |
| Deny(reason) | Rejected | Notify Agent |
| Project(alt) | Projected to a feasible solution | Execute alt |
| Delay(duration) | Delayed execution | Retry after waiting |
| ManualReview | Manual review required | Escalate 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
| Rule | Check Content | Applicable Equipment |
|---|
| no_load_break_disconnector | Prohibit disconnecting a disconnector under load | Disconnector |
| sync_check_before_close | Synchronization check before closing | Circuit breaker |
| five_interlocks | Full five-interlock check | All equipment |
| maintenance_zone_isolation | Maintenance zone isolation | All equipment |
| frequency_block | Frequency limit blocking | System-level |
| voltage_block | Voltage limit blocking | Bus-level |
| overload_block | Equipment overload blocking | Line, transformer |
| cold_load_pickup | Cold load pickup limit | Load switch |
| reclosing_block | Reclosing blocking | Circuit breaker |
| parallel_protection | Parallel protection | Generator |
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
| Field | Type | Description |
|---|
| id | TicketId | Unique operation ticket ID |
| steps | Vec | Operation step sequence |
| created_by | AgentId | Creator |
| approved_by | Option | Approver (for critical operations) |
| status | TicketStatus | Draft / Pending / Active / Done / Cancelled |
| timeout | Duration | Auto-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
| Strategy | Meaning | Applicable Scenario |
|---|
| TokenBucket | Token bucket | General Agent |
| LeakyBucket | Leaky bucket | Smooth traffic |
| SlidingWindow | Sliding window | Precise rate limiting |
| Quota | Total quota | Critical 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
| Field | Type | Description |
|---|
| timestamp | Timestamp | Command submission time |
| agent | AgentId | Submitter |
| command | Command | Command content |
| verdict | Verdict | Decision result |
| reason | Option | Reason for rejection |
| duration_us | u64 | Validation cost (μs) |
| tenant | TenantId | Tenant |
| 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
| Variant | Scope | Recovery Method |
|---|
| All | Whole system | Manual |
| Region(name) | Specified region | Manual |
| Agent(id) | Single Agent | Automatic |
| Device(id) | Single device | Automatic |
Safety Level Matrix
| Level | Description | Stages | Applicable Scenario |
|---|
| L0 | Fully open | Audit only | Lab testing |
| L1 | Constraint validation | Auth + Constraint | Digital twin |
| L2 | Constraint + rules | + Rule + RateLimit | Production environment |
| L3 | Full chain + operation ticket | + Permission + Ticket | Critical infrastructure |
| L4 | L3 + independent hardware validation | + Hardware Safety Module | National 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?;
| Operation | Latency | Throughput |
|---|
| Single command validation (full chain) | < 100μs | 10k 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μs | 100k 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
| Parameter | Type | Default | Description |
|---|
| level | enum | L3 | Safety level |
| require_ticket | bool | true | Enforce operation tickets |
| default_rate_limit | u32 | 100 | Default rate limit |
| critical_device_quota | u32 | 5 | Critical device quota |
| audit_dir | String | /var/eneros/audit | WORM audit path |
| audit_retention_days | u32 | 3650 | Audit retention period |
| hardware_emergency_stop | bool | true | Hardware emergency stop |
| emergency_heartbeat_ms | u32 | 100 | Heartbeat timeout |
Relationship with Other Capabilities
| Related Capability | Interaction |
|---|
| Physical Constraint Decision | The constraint engine is the core of the gateway |
| Zero Trust and Security Enhancement | Identity authentication and audit |
| Multi-Agent Collaboration | Source of Agent commands |
| Real-Time Dual Execution Domain | Real-time domain commands also pass through the gateway |
| Multi-tenant and Isolation | Commands are audited per tenant namespace |
| Time-Series Native Operations | Audit logs are ingested |
| Grid Topology First-Class Citizen | Operation tickets depend on topology validation |