You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SecurityModeManager::evaluate_threat accepts a caller-supplied score and, at the critical threshold, enters Panic firewall policy and invokes the modem power-cut path. The function is still unreferenced outside tests but is compiled and tested. It contradicts the active Phase-10 contract that uncalibrated detector output is advisory and cannot autonomously cut radios or enter Panic.
What #885 changed is the consequence, not the authorization gap. The cut path now records a request instead of performing MMIO, so the same uncalibrated score can no longer reach hardware — but it still autonomously drives Panic firewall mode and a sticky requested state, and it will reach hardware the moment #862 lands a working seam behind it.
Evidence
Current tree, main at a1154631cd03a212636e36407ba73d21d675fff0:
crates/thumos/src/security_mode.rs:808-826 — evaluate_threat compares a raw score to a caller-supplied threshold and, above it, calls firewall.apply_mode(FirewallMode::Panic) and power_manager.request_modem_power_cut(). Two FIXME(#874) markers sit on those exact lines.
crates/thumos/src/security_mode.rs:1546,1577,1604,1630 — every call site of evaluate_threat is inside #[cfg(test)]. It remains dead-but-public policy.
crates/thumos/src/power.rs:459-471 — request_modem_power_cut sets PowerState::PowerCutRequested and performs no hardware operation.
crates/thumos/src/security_mode.rs:791 — the response renders as modem cut requested (unapplied).
Phase-10 REQ-05/08 require versioned corpus calibration, uncertainty, and no automatic destructive or radio-cut action.
The earlier Evidence text said evaluate_threat "directly calls modem_power_cut", describing a physical kill. That symbol no longer exists; #885 replaced it with the request-only path above.
The requested/observed fix on the UI side has no regression coverage
crates/thumos/src/kardia.rs:538-543 is the corrected threat indicator: threat_high now derives from self.threat.detector_online() && matches!(self.threat.threat_level(), High | Critical) rather than from CCCI boot-path absence. That fix is correct and is exactly the requested-versus-observed separation this issue asks for.
Nothing would catch its reversion:
crates/thumos/src/kardia.rs has no #[cfg(test)] module at all.
scripts/witness/boot.sh:92 asserts modem_path_available=false in the threat-monitor log line, and never asserts threat_high.
So a revert to the old !self.boot.modem_ok derivation would pass the full suite and the boot witness. Per VERIFICATION.md, a claim without a failing fixture cannot be passed — this one landed without one, which makes it fixed-until-the-next-refactor rather than durably fixed.
Why this matters
Dead-but-public policy invites a future caller to treat a tested API as accepted security behaviour, and this one now sits directly upstream of a seam that is being built. The ordering is the hazard: if #862 lands a working PWRAP transaction before this authorization policy exists, an arbitrary or false-positive score becomes a real loss of connectivity with no operator in the loop, and nothing in #862's own acceptance criteria would catch that — it is a correctness contract for the transport, not for who is allowed to call it.
The untested UI fix compounds the same problem from the other direction. The whole point of separating requested from observed state is that an operator can trust the badge; a derivation nothing guards can silently drift back to reporting a boot-path artefact as a threat.
Done when
Remove or redesign score-to-action coupling so detector outputs carry source, policy version, uncertainty, calibration status, freshness, and evidence; uncalibrated output is advisory only.
Separate requested, software-applied, independently observed, failed, and unknown state in the API, logs, UI, docs, and tests.
The kardia.rs threat-indicator derivation is covered by a regression test over the detector_online × threat_level cross product, or the boot witness asserts threat_high directly. Extracting the expression to a pure function is the cheap route.
Keep physical kill claims impossible until a source-grounded driver/PMIC seam and an independent receipt exist.
This issue gates activation of #862's callers; #862 may land its transport without waiting on this, but nothing may call it on hardware until this is resolved.
Finding
SecurityModeManager::evaluate_threataccepts a caller-supplied score and, at the critical threshold, enters Panic firewall policy and invokes the modem power-cut path. The function is still unreferenced outside tests but is compiled and tested. It contradicts the active Phase-10 contract that uncalibrated detector output is advisory and cannot autonomously cut radios or enter Panic.What #885 changed is the consequence, not the authorization gap. The cut path now records a request instead of performing MMIO, so the same uncalibrated score can no longer reach hardware — but it still autonomously drives Panic firewall mode and a sticky requested state, and it will reach hardware the moment #862 lands a working seam behind it.
Evidence
Current tree,
mainata1154631cd03a212636e36407ba73d21d675fff0:crates/thumos/src/security_mode.rs:808-826—evaluate_threatcompares a raw score to a caller-supplied threshold and, above it, callsfirewall.apply_mode(FirewallMode::Panic)andpower_manager.request_modem_power_cut(). TwoFIXME(#874)markers sit on those exact lines.crates/thumos/src/security_mode.rs:1546,1577,1604,1630— every call site ofevaluate_threatis inside#[cfg(test)]. It remains dead-but-public policy.crates/thumos/src/power.rs:459-471—request_modem_power_cutsetsPowerState::PowerCutRequestedand performs no hardware operation.crates/thumos/src/security_mode.rs:791— the response renders asmodem cut requested (unapplied).The earlier Evidence text said
evaluate_threat"directly callsmodem_power_cut", describing a physical kill. That symbol no longer exists; #885 replaced it with the request-only path above.The requested/observed fix on the UI side has no regression coverage
crates/thumos/src/kardia.rs:538-543is the corrected threat indicator:threat_highnow derives fromself.threat.detector_online() && matches!(self.threat.threat_level(), High | Critical)rather than from CCCI boot-path absence. That fix is correct and is exactly the requested-versus-observed separation this issue asks for.Nothing would catch its reversion:
crates/thumos/src/kardia.rshas no#[cfg(test)]module at all.scripts/witness/boot.sh:92assertsmodem_path_available=falsein the threat-monitor log line, and never assertsthreat_high.So a revert to the old
!self.boot.modem_okderivation would pass the full suite and the boot witness. PerVERIFICATION.md, a claim without a failing fixture cannot be passed — this one landed without one, which makes it fixed-until-the-next-refactor rather than durably fixed.Why this matters
Dead-but-public policy invites a future caller to treat a tested API as accepted security behaviour, and this one now sits directly upstream of a seam that is being built. The ordering is the hazard: if #862 lands a working PWRAP transaction before this authorization policy exists, an arbitrary or false-positive score becomes a real loss of connectivity with no operator in the loop, and nothing in #862's own acceptance criteria would catch that — it is a correctness contract for the transport, not for who is allowed to call it.
The untested UI fix compounds the same problem from the other direction. The whole point of separating requested from observed state is that an operator can trust the badge; a derivation nothing guards can silently drift back to reporting a boot-path artefact as a threat.
Done when
kardia.rsthreat-indicator derivation is covered by a regression test over thedetector_online×threat_levelcross product, or the boot witness assertsthreat_highdirectly. Extracting the expression to a pure function is the cheap route.This issue gates activation of #862's callers; #862 may land its transport without waiting on this, but nothing may call it on hardware until this is resolved.