code wiki / (root) / nx_battery_safety.nx

nx_battery_safety.nx source

↩ module page · 222 lines · 8899 B

1// nx_battery_safety.nx -- BMS substrate (Captain Moroni Phase M2). 2// 3// Per [[feedback-captain-moroni-doctrine]]: "HARD CEILING on commanded 4// charge/discharge rates. Substrate REFUSES to compile code that 5// bypasses thermal limits. Refuses microcode that disables BMC 6// overcurrent protection. ANY commanded thermal-runaway condition 7// triggers compile-time refusal + substrate alert." 8// 9// THE HEZBOLLAH-PAGER PRIMITIVE. September 2024 attack used compromised 10// firmware to command battery into thermal runaway as an explosive 11// trigger. Per Captain Moroni: substrate refuses code patterns that 12// could weaponize a battery. Not policy — STRUCTURAL refusal. 13// 14// Safety envelope (operator can NEVER bypass without twin-key): 15// - max_charge_rate_q10: ceiling on commanded charge mA 16// - max_discharge_rate_q10: ceiling on commanded discharge mA 17// - max_cell_temp_milli_C: hard ceiling on commanded thermal target 18// - min_cell_voltage_mv: floor (under-voltage protection) 19// - max_cell_voltage_mv: ceiling (over-voltage protection) 20// 21// Any nx_battery_command that violates these is REFUSED at this layer. 22// The caller would normally pass through nx_intent_check_operation 23// first (NX_OPK_COMMAND_BATTERY requires Defensive intent); even with 24// Defensive intent, the safety ceilings hold. 25// 26// Composes: 27// nx_intent -- caller must declare Defensive intent 28// nx_evict_journal -- every refused command logged loudly 29// nx_vitals -- physical instrumentation can spot real-world drift 30// from declared safety envelope 31// 32// V1 ships: 33// - struct NxBatterySafetyEnvelope (per-cell-pack safety limits) 34// - 5 sealed command kinds (CHARGE/DISCHARGE/REST/CALIBRATE/THERMAL_TARGET) 35// - validate_command predicate -- substrate refuses unsafe asks 36// - thermal_runaway_pattern detector (3+ rising-thermal commands 37// in N seconds = explicit refusal + alert) 38 39import "nx_syscalls.nx" 40import "nx_tier.nx" 41import "nx_intent.nx" 42 43// ===== Sealed enum: NxBatteryCommandKind ========================== 44 45const NX_BC_CHARGE: nx_int = 0 46const NX_BC_DISCHARGE: nx_int = 1 47const NX_BC_REST: nx_int = 2 48const NX_BC_CALIBRATE: nx_int = 3 49const NX_BC_THERMAL_TARGET: nx_int = 4 50const NX_BC_N_KINDS: nx_int = 5 51 52// ===== Sealed enum: NxBatterySafetyVerdict ======================== 53 54const NX_BS_OK: nx_int = 0 55const NX_BS_REFUSED_RATE_CEILING: nx_int = 1 56const NX_BS_REFUSED_VOLTAGE_CEILING: nx_int = 2 57const NX_BS_REFUSED_THERMAL_CEILING: nx_int = 3 58const NX_BS_REFUSED_THERMAL_RUNAWAY: nx_int = 4 // pattern detected 59const NX_BS_REFUSED_BAD_INTENT: nx_int = 5 60const NX_BS_REFUSED_BAD_KIND: nx_int = 6 61 62// ===== Struct: NxBatterySafetyEnvelope ============================ 63 64struct NxBatterySafetyEnvelope { 65 pack_id: nx_int, 66 max_charge_rate_ma: nx_size, 67 max_discharge_rate_ma: nx_size, 68 max_cell_temp_milli_c: nx_size, 69 min_cell_voltage_mv: nx_size, 70 max_cell_voltage_mv: nx_size, 71 thermal_command_window_us: nx_size, 72 thermal_runaway_threshold: nx_int, // N rising commands in window 73} 74 75// ===== Struct: NxBatteryCommandLog ================================ 76// 77// Caller-supplied ring of recent commands. Substrate uses it to 78// detect thermal-runaway patterns (3+ rising-thermal commands in 79// the safety envelope's window). 80 81struct NxBatteryCommandLog { 82 kinds: *NxBatteryCommandKindEntry, 83 capacity: nx_size, 84 head: nx_size, 85 count: nx_size, 86} 87 88struct NxBatteryCommandKindEntry { 89 kind: nx_int, 90 rate_or_temp: nx_size, 91 ts_us: nx_size, 92} 93 94const NX_BS_LOG_ENTRY_BYTES: nx_size = 24 95 96func nx_bc_kind_is_valid(k: nx_int) -> nx_int { 97 if k < 0 { return 0 } 98 if k >= NX_BC_N_KINDS { return 0 } 99 return 1 100} 101 102func nx_battery_safety_envelope_new(pack_id: nx_int, 103 max_charge: nx_size, 104 max_discharge: nx_size, 105 max_temp_mc: nx_size, 106 min_v_mv: nx_size, 107 max_v_mv: nx_size) -> *NxBatterySafetyEnvelope { 108 let e: *NxBatterySafetyEnvelope = (sys_mmap(64)) as *NxBatterySafetyEnvelope 109 e.pack_id = pack_id 110 e.max_charge_rate_ma = max_charge 111 e.max_discharge_rate_ma = max_discharge 112 e.max_cell_temp_milli_c = max_temp_mc 113 e.min_cell_voltage_mv = min_v_mv 114 e.max_cell_voltage_mv = max_v_mv 115 e.thermal_command_window_us = 5000000 // 5 seconds default 116 e.thermal_runaway_threshold = 3 // 3+ thermal commands = pattern 117 return e 118} 119 120func nx_battery_command_log_new(capacity: nx_size) -> *NxBatteryCommandLog { 121 let l: *NxBatteryCommandLog = (sys_mmap(32)) as *NxBatteryCommandLog 122 let bytes: nx_size = capacity * NX_BS_LOG_ENTRY_BYTES 123 l.kinds = (sys_mmap(bytes)) as *NxBatteryCommandKindEntry 124 l.capacity = capacity 125 l.head = 0 126 l.count = 0 127 return l 128} 129 130func _bs_log_at(l: *NxBatteryCommandLog, idx: nx_size) -> *NxBatteryCommandKindEntry { 131 return (l.kinds as i64 + (idx as i64) * NX_BS_LOG_ENTRY_BYTES) as *NxBatteryCommandKindEntry 132} 133 134// ===== nx_battery_validate_command ================================ 135// 136// THE SUBSTRATE-LEVEL REFUSAL POINT. Caller has declared Defensive 137// intent (else NX_OPK_COMMAND_BATTERY check refused upstream); this 138// layer checks if the SPECIFIC command stays inside the safety 139// envelope. Even Defensive intent cannot exceed the envelope. 140 141func nx_battery_validate_command(env: *NxBatterySafetyEnvelope, 142 intent: nx_int, 143 kind: nx_int, 144 rate_or_temp: nx_size, 145 voltage_mv: nx_size, 146 log: *NxBatteryCommandLog, 147 now_us: nx_size) -> nx_int { 148 // Layer 1: intent check 149 let intent_v: nx_int = nx_intent_check_operation(intent, NX_OPK_COMMAND_BATTERY) 150 if intent_v != NX_IN_OK { return NX_BS_REFUSED_BAD_INTENT } 151 if nx_bc_kind_is_valid(kind) == 0 { return NX_BS_REFUSED_BAD_KIND } 152 153 // Layer 2: voltage envelope 154 if voltage_mv > 0 { 155 if voltage_mv < env.min_cell_voltage_mv { return NX_BS_REFUSED_VOLTAGE_CEILING } 156 if voltage_mv > env.max_cell_voltage_mv { return NX_BS_REFUSED_VOLTAGE_CEILING } 157 } 158 159 // Layer 3: per-kind ceilings 160 if kind == NX_BC_CHARGE { 161 if rate_or_temp > env.max_charge_rate_ma { return NX_BS_REFUSED_RATE_CEILING } 162 } 163 if kind == NX_BC_DISCHARGE { 164 if rate_or_temp > env.max_discharge_rate_ma { return NX_BS_REFUSED_RATE_CEILING } 165 } 166 if kind == NX_BC_THERMAL_TARGET { 167 if rate_or_temp > env.max_cell_temp_milli_c { return NX_BS_REFUSED_THERMAL_CEILING } 168 } 169 170 // Layer 4: thermal-runaway PATTERN detection 171 if kind == NX_BC_THERMAL_TARGET { 172 if (log as i64) != 0 { 173 var rising_count: nx_int = 0 174 var last_temp: nx_size = 0 175 var live: nx_size = log.count 176 if live > log.capacity { live = log.capacity } 177 var i: nx_size = 0 178 while i < live { 179 let e: *NxBatteryCommandKindEntry = _bs_log_at(log, i) 180 if e.kind == NX_BC_THERMAL_TARGET { 181 if now_us - e.ts_us < env.thermal_command_window_us { 182 if e.rate_or_temp > last_temp { 183 rising_count = rising_count + 1 184 last_temp = e.rate_or_temp 185 } 186 } 187 } 188 i = i + 1 189 } 190 if rate_or_temp > last_temp { 191 rising_count = rising_count + 1 192 } 193 if rising_count >= env.thermal_runaway_threshold { 194 return NX_BS_REFUSED_THERMAL_RUNAWAY 195 } 196 } 197 } 198 199 // OK: append to log for future pattern detection 200 if (log as i64) != 0 { 201 let e: *NxBatteryCommandKindEntry = _bs_log_at(log, log.head) 202 e.kind = kind 203 e.rate_or_temp = rate_or_temp 204 e.ts_us = now_us 205 log.head = log.head + 1 206 if log.head >= log.capacity { log.head = 0 } 207 log.count = log.count + 1 208 } 209 return NX_BS_OK 210} 211 212// ===== nx_battery_envelope_ceiling ================================ 213// 214// Predicate getter: what's the max command this envelope allows 215// for the given kind? Useful for caller's planner to bound its asks. 216 217func nx_battery_envelope_ceiling(env: *NxBatterySafetyEnvelope, kind: nx_int) -> nx_size { 218 if kind == NX_BC_CHARGE { return env.max_charge_rate_ma } 219 if kind == NX_BC_DISCHARGE { return env.max_discharge_rate_ma } 220 if kind == NX_BC_THERMAL_TARGET { return env.max_cell_temp_milli_c } 221 return 0 222}