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}