From 3b2c3ec4800f41c71c0b27747af83b36696ff57e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:30:45 +0000 Subject: [PATCH 1/3] docs: mark 0.3.0 install examples post-publication Split the crates.io dependency snippets so default vs corpus-ipc are not the same TOML table, describe 0.3.0 as pending publish, and add an OS child-process smoke that a parent thermal fault cannot leak onto the wire or Brainstem health snapshot. Co-authored-by: Raul Cardenas Montoya --- CHANGELOG.md | 21 ++++-- README.md | 23 +++++-- tests/thalamic_brainstem_smoke.rs | 108 +++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a8cf5..7872c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.3.0] - 2026-09-18 -crates.io release matching GitHub milestone **v0.3.0 — Spikenaut SNN runtime**. -This crate was last published as **0.1.2** (April 2026, `neuromod` 0.4.0). There -is no 0.2.0 crates.io line; that GitHub milestone was stub-quality work. +Pending crates.io publication for GitHub milestone **v0.3.0 — Spikenaut SNN runtime**. +In-tree package version is **0.3.0**; `cargo publish` has not run. Last published +crate remains **0.1.2** (April 2026, `neuromod` 0.4.0). There is no 0.2.0 +crates.io line; that GitHub milestone was stub-quality work. ### Added @@ -23,6 +24,10 @@ is no 0.2.0 crates.io line; that GitHub milestone was stub-quality work. - Smoke coverage that a dropped/reconstructed Thalamic producer keeps hardware-safety state process-local while Brainstem still ticks a loaded checkpoint (`thalamic_restart_keeps_hardware_safety_out_of_brainstem`). +- OS-process Thalamic restart smoke: a child process starts with a clean + safety flag; the parent's thermal fault is absent from the published + `IpcMessage` JSON and from Brainstem `HealthSnapshot` + (`thalamic_os_process_restart_does_not_leak_safety`). - Distinct liveness, readiness, recoverable degradation, and sticky fatal health (`src/health/`) with a fake-clock state-machine test for every transition and recovery path. Optional `control_bind` listener serves `/livez`, `/readyz`, @@ -111,10 +116,12 @@ is no 0.2.0 crates.io line; that GitHub milestone was stub-quality work. - Binary now logs the active backend mode (`🔌 stub` / `📡 ZMQ corpus-ipc`). - `decode_inputs` now accepts `&IngressPacket` (with explicit `None` modulator fallback). - All direct `corpus_ipc` / `zmq` usage is now feature-gated (except the compatibility `CORPUS_IPC_READOUT_ENV` const). -- Package version **0.3.0** (`Cargo.toml` / `Cargo.lock`). Install and - dependency snippets use `brainstem-daemon = "0.3.0"`. This PR does not - create a Git tag or GitHub Release; `cargo publish` remains a maintainer - step after merge. +- Package version **0.3.0** (`Cargo.toml` / `Cargo.lock`). README install and + dependency snippets are **post-publication** (`brainstem-daemon = "0.3.0"` + once the crate is on crates.io). Default vs `corpus-ipc` examples are + separate tables so they cannot be pasted as a duplicate TOML key. This + changelog section is pending registry publication; `cargo publish`, a Git + tag, and a GitHub Release remain maintainer steps. ### Removed diff --git a/README.md b/README.md index 25540f0..f4dfb5d 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,34 @@ Thalamic can be stopped and started again without Brainstem taking over hardware ## Install -From crates.io (binary): +**0.3.0 is prepared in-tree but is not on crates.io yet.** `cargo publish` has +not run; the last published crate is **0.1.2**. Until publication, +`brainstem-daemon = "0.3.0"` does not resolve from the registry (use a git or +path dependency for development). After 0.3.0 is published: + +From crates.io (binary) — pick **one**: ```bash cargo install brainstem-daemon +``` + +```bash # Optional ZeroMQ / corpus-ipc backend (needs a C/C++ toolchain; system libzmq is optional): cargo install brainstem-daemon --features corpus-ipc ``` -As a library dependency (crates.io, not a git pin): +As a library dependency (crates.io, not a git pin) — pick **one** table; do not +paste both keys into the same `Cargo.toml`: + +Default stub backend (no ZeroMQ): ```toml brainstem-daemon = "0.3.0" -# Optional ZeroMQ backend: +``` + +Optional ZeroMQ / `corpus-ipc` backend: + +```toml brainstem-daemon = { version = "0.3.0", features = ["corpus-ipc"] } ``` @@ -255,7 +270,7 @@ Live restore copies LIF weights, membrane, `last_spike`, `decay_rate`, and `thre CPU-only integration coverage (no GPU) lives in `tests/thalamic_brainstem_smoke.rs` and is gated on `--features corpus-ipc` so default stub tests never need `libzmq`. -The Thalamic fixture (`tests/fixtures/thalamic_producer.rs`) produces `IpcMessage::Stimuli(StimulusBatch)` from simulated telemetry. It does not import `neuromod` or own a `SpikingNetwork`. Brainstem restores a Distill sidecar JSON checkpoint before ticking, preserves `valid_mask` and `session_id` across the wire, and rejects incompatible schema/JSON loudly. A separate assertion keeps the fixture's safety flag healthy when Brainstem/transport is absent, and a later publish cannot clobber a thermal fault. Dropping the producer and constructing a new one (Thalamic restart) resets only that process-local safety flag; Brainstem health stays liveness/readiness/checkpoint and never inherits thermal/power duty. +The Thalamic fixture (`tests/fixtures/thalamic_producer.rs`) produces `IpcMessage::Stimuli(StimulusBatch)` from simulated telemetry. It does not import `neuromod` or own a `SpikingNetwork`. Brainstem restores a Distill sidecar JSON checkpoint before ticking, preserves `valid_mask` and `session_id` across the wire, and rejects incompatible schema/JSON loudly. A separate assertion keeps the fixture's safety flag healthy when Brainstem/transport is absent, and a later publish cannot clobber a thermal fault. Dropping the producer and constructing a new one (in-process restart) resets only that process-local safety flag. A child OS process (`thalamic_os_process_restart_does_not_leak_safety`) starts healthy, publishes a wire frame with no thermal/safety keys, and Brainstem `HealthSnapshot` still has no thermal/power fields. ```bash CC=gcc CXX=g++ cargo test --locked --features corpus-ipc --test thalamic_brainstem_smoke diff --git a/tests/thalamic_brainstem_smoke.rs b/tests/thalamic_brainstem_smoke.rs index 9c2a458..ff1dfa9 100644 --- a/tests/thalamic_brainstem_smoke.rs +++ b/tests/thalamic_brainstem_smoke.rs @@ -10,12 +10,15 @@ mod thalamic; use std::collections::VecDeque; use std::path::{Path, PathBuf}; +use std::process::Command; use std::time::Duration; use anyhow::Result; use brainstem_daemon::daemon::{BrainstemDaemon, DaemonConfig, RuntimeMode}; use brainstem_daemon::ingress::{IngressConfig, IngressPolicy, accept_ipc_json}; -use brainstem_daemon::{BackendPair, CollectingSpikeSink, IngressPacket, StimulusSource}; +use brainstem_daemon::{ + BackendPair, CollectingSpikeSink, HealthSnapshot, IngressPacket, StimulusSource, +}; use corpus_ipc::{IpcMessage, StimulusBatch}; use thalamic::{StimulusTransport, ThalamicProducer}; @@ -363,3 +366,106 @@ fn thalamic_restart_keeps_hardware_safety_out_of_brainstem() { restarted.safety_tick(false); assert!(!restarted.safety_healthy); } + +const OS_RESTART_CHILD_ENV: &str = "BRAINSTEM_THALAMIC_OS_RESTART_CHILD"; +const OS_RESTART_FRAME_ENV: &str = "BRAINSTEM_THALAMIC_OS_RESTART_FRAME"; + +/// True if `key` appears anywhere in a JSON object tree (map keys only). +fn json_has_key(value: &serde_json::Value, key: &str) -> bool { + match value { + serde_json::Value::Object(map) => { + map.contains_key(key) || map.values().any(|v| json_has_key(v, key)) + } + serde_json::Value::Array(items) => items.iter().any(|v| json_has_key(v, key)), + _ => false, + } +} + +fn assert_no_hardware_safety_keys(value: &serde_json::Value, where_: &str) { + for key in [ + "safety_healthy", + "thermal", + "nvml", + "gpu_power", + "power_limit", + ] { + assert!( + !json_has_key(value, key), + "{where_} must not carry hardware-safety key `{key}`: {value}" + ); + } +} + +#[test] +fn thalamic_os_process_restart_does_not_leak_safety() { + // Child OS process: fresh address space, no parent `ThalamicProducer`. + if let Ok(frame_path) = std::env::var(OS_RESTART_FRAME_ENV) { + assert!( + std::env::var_os(OS_RESTART_CHILD_ENV).is_some(), + "child must be marked as a new process, not an in-process reconstruct" + ); + let mut child = ThalamicProducer::new(); + assert!( + child.safety_healthy, + "new OS process must not inherit the parent's thermal fault" + ); + let batch = child.simulate_telemetry(11, CHANNELS); + let bytes = child.encode_frame(&batch).expect("child encode"); + std::fs::write(&frame_path, bytes).expect("write child frame"); + return; + } + + let dir = TempDir::new("brainstem-smoke-os-restart"); + let sidecar = write_smoke_sidecar(dir.path()); + let frame_path = dir.path().join("child_frame.json"); + + let mut parent = ThalamicProducer::new(); + parent.safety_tick(false); + assert!(!parent.safety_healthy); + + let exe = std::env::current_exe().expect("current_exe"); + let output = Command::new(&exe) + .env(OS_RESTART_CHILD_ENV, "1") + .env(OS_RESTART_FRAME_ENV, &frame_path) + .args([ + "thalamic_os_process_restart_does_not_leak_safety", + "--exact", + ]) + .output() + .expect("spawn Thalamic child process"); + assert!( + output.status.success(), + "child failed status={:?} stdout={} stderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + // Parent process still holds its own thermal fault after the child exits. + assert!(!parent.safety_healthy); + + let bytes = std::fs::read(&frame_path).expect("child wrote a frame"); + let wire: serde_json::Value = serde_json::from_slice(&bytes).expect("child JSON"); + assert_no_hardware_safety_keys(&wire, "child IpcMessage"); + + let policy = IngressPolicy::new(CHANNELS, Some(Duration::from_secs(1))); + let packet = accept_ipc_json(&bytes, &policy, now_ns()).unwrap(); + + let source = QueuedStimulusSource { + packets: VecDeque::from([Ok(packet)]), + }; + let sink = CollectingSpikeSink::new(); + let pair = BackendPair { + source: Box::new(source), + sink: Box::new(sink), + }; + let daemon = BrainstemDaemon::try_with_backend(smoke_config(sidecar), pair).unwrap(); + let snap: HealthSnapshot = daemon.health().snapshot(); + let snap_json = serde_json::to_value(&snap).expect("snapshot json"); + assert_no_hardware_safety_keys(&snap_json, "Brainstem HealthSnapshot"); + + let stats = daemon.run_for_ticks(1).unwrap(); + assert_eq!(stats.ticks, 1); + assert_eq!(stats.accepted_batches, 1); + assert!(stats.loaded_checkpoint.is_some()); +} From bae6ff8721b6c92800be027d0297af2c08ccc7c2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:31:52 +0000 Subject: [PATCH 2/3] test: drop unused mut on Thalamic OS-restart child Co-authored-by: Raul Cardenas Montoya --- tests/thalamic_brainstem_smoke.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/thalamic_brainstem_smoke.rs b/tests/thalamic_brainstem_smoke.rs index ff1dfa9..932bfc8 100644 --- a/tests/thalamic_brainstem_smoke.rs +++ b/tests/thalamic_brainstem_smoke.rs @@ -404,7 +404,7 @@ fn thalamic_os_process_restart_does_not_leak_safety() { std::env::var_os(OS_RESTART_CHILD_ENV).is_some(), "child must be marked as a new process, not an in-process reconstruct" ); - let mut child = ThalamicProducer::new(); + let child = ThalamicProducer::new(); assert!( child.safety_healthy, "new OS process must not inherit the parent's thermal fault" From 4cd90b6542372d093f9e19f58f55695c32207306 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 00:36:14 +0000 Subject: [PATCH 3/3] test: harden Thalamic OS-restart smoke against review nits Enter child mode only with an explicit parent token, re-exec via argv0 instead of current_exe, split the test under Codacy's line limit, and widen ingress freshness so slow CI cannot flake the child frame. Co-authored-by: Raul Cardenas Montoya --- tests/thalamic_brainstem_smoke.rs | 102 +++++++++++++++++------------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/tests/thalamic_brainstem_smoke.rs b/tests/thalamic_brainstem_smoke.rs index 932bfc8..65ec185 100644 --- a/tests/thalamic_brainstem_smoke.rs +++ b/tests/thalamic_brainstem_smoke.rs @@ -369,6 +369,8 @@ fn thalamic_restart_keeps_hardware_safety_out_of_brainstem() { const OS_RESTART_CHILD_ENV: &str = "BRAINSTEM_THALAMIC_OS_RESTART_CHILD"; const OS_RESTART_FRAME_ENV: &str = "BRAINSTEM_THALAMIC_OS_RESTART_FRAME"; +/// Parent-only token so an inherited empty/`1` env var cannot enter child mode. +const OS_RESTART_CHILD_TOKEN: &str = "brainstem-thalamic-os-restart-v1"; /// True if `key` appears anywhere in a JSON object tree (map keys only). fn json_has_key(value: &serde_json::Value, key: &str) -> bool { @@ -396,22 +398,63 @@ fn assert_no_hardware_safety_keys(value: &serde_json::Value, where_: &str) { } } +fn os_restart_child_frame_path() -> Option { + match std::env::var(OS_RESTART_CHILD_ENV) { + Ok(token) if token == OS_RESTART_CHILD_TOKEN => { + std::env::var_os(OS_RESTART_FRAME_ENV).map(PathBuf::from) + } + _ => None, + } +} + +fn write_os_restart_child_frame(frame_path: &Path) { + let child = ThalamicProducer::new(); + assert!( + child.safety_healthy, + "new OS process must not inherit the parent's thermal fault" + ); + let bytes = child + .encode_frame(&child.simulate_telemetry(11, CHANNELS)) + .expect("child encode"); + std::fs::write(frame_path, bytes).expect("write child frame"); +} + +fn spawn_os_restart_child(frame_path: &Path) -> std::process::Output { + let argv0 = std::env::args_os().next().expect("argv0"); + Command::new(argv0) + .env(OS_RESTART_CHILD_ENV, OS_RESTART_CHILD_TOKEN) + .env(OS_RESTART_FRAME_ENV, frame_path) + .args([ + "thalamic_os_process_restart_does_not_leak_safety", + "--exact", + ]) + .output() + .expect("spawn Thalamic child process") +} + +fn tick_brainstem_on_child_frame(sidecar: PathBuf, bytes: &[u8]) -> brainstem_daemon::RuntimeStats { + // Child startup + CI scheduling can exceed 1s; this test is about safety + // isolation, not ingress freshness. + let policy = IngressPolicy::new(CHANNELS, Some(Duration::from_secs(60))); + let packet = accept_ipc_json(bytes, &policy, now_ns()).unwrap(); + let source = QueuedStimulusSource { + packets: VecDeque::from([Ok(packet)]), + }; + let pair = BackendPair { + source: Box::new(source), + sink: Box::new(CollectingSpikeSink::new()), + }; + let daemon = BrainstemDaemon::try_with_backend(smoke_config(sidecar), pair).unwrap(); + let snap: HealthSnapshot = daemon.health().snapshot(); + let snap_json = serde_json::to_value(&snap).expect("snapshot json"); + assert_no_hardware_safety_keys(&snap_json, "Brainstem HealthSnapshot"); + daemon.run_for_ticks(1).unwrap() +} + #[test] fn thalamic_os_process_restart_does_not_leak_safety() { - // Child OS process: fresh address space, no parent `ThalamicProducer`. - if let Ok(frame_path) = std::env::var(OS_RESTART_FRAME_ENV) { - assert!( - std::env::var_os(OS_RESTART_CHILD_ENV).is_some(), - "child must be marked as a new process, not an in-process reconstruct" - ); - let child = ThalamicProducer::new(); - assert!( - child.safety_healthy, - "new OS process must not inherit the parent's thermal fault" - ); - let batch = child.simulate_telemetry(11, CHANNELS); - let bytes = child.encode_frame(&batch).expect("child encode"); - std::fs::write(&frame_path, bytes).expect("write child frame"); + if let Some(frame_path) = os_restart_child_frame_path() { + write_os_restart_child_frame(&frame_path); return; } @@ -423,16 +466,7 @@ fn thalamic_os_process_restart_does_not_leak_safety() { parent.safety_tick(false); assert!(!parent.safety_healthy); - let exe = std::env::current_exe().expect("current_exe"); - let output = Command::new(&exe) - .env(OS_RESTART_CHILD_ENV, "1") - .env(OS_RESTART_FRAME_ENV, &frame_path) - .args([ - "thalamic_os_process_restart_does_not_leak_safety", - "--exact", - ]) - .output() - .expect("spawn Thalamic child process"); + let output = spawn_os_restart_child(&frame_path); assert!( output.status.success(), "child failed status={:?} stdout={} stderr={}", @@ -440,31 +474,13 @@ fn thalamic_os_process_restart_does_not_leak_safety() { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - - // Parent process still holds its own thermal fault after the child exits. assert!(!parent.safety_healthy); let bytes = std::fs::read(&frame_path).expect("child wrote a frame"); let wire: serde_json::Value = serde_json::from_slice(&bytes).expect("child JSON"); assert_no_hardware_safety_keys(&wire, "child IpcMessage"); - let policy = IngressPolicy::new(CHANNELS, Some(Duration::from_secs(1))); - let packet = accept_ipc_json(&bytes, &policy, now_ns()).unwrap(); - - let source = QueuedStimulusSource { - packets: VecDeque::from([Ok(packet)]), - }; - let sink = CollectingSpikeSink::new(); - let pair = BackendPair { - source: Box::new(source), - sink: Box::new(sink), - }; - let daemon = BrainstemDaemon::try_with_backend(smoke_config(sidecar), pair).unwrap(); - let snap: HealthSnapshot = daemon.health().snapshot(); - let snap_json = serde_json::to_value(&snap).expect("snapshot json"); - assert_no_hardware_safety_keys(&snap_json, "Brainstem HealthSnapshot"); - - let stats = daemon.run_for_ticks(1).unwrap(); + let stats = tick_brainstem_on_child_frame(sidecar, &bytes); assert_eq!(stats.ticks, 1); assert_eq!(stats.accepted_batches, 1); assert!(stats.loaded_checkpoint.is_some());