From 5993f321463e782a0d2e5546d5b426c92156a40f Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Wed, 22 Apr 2026 20:56:42 +0800 Subject: [PATCH] feat(v0.22): FS warn + battle-test matrix + USB docs platform split (Track C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Filesystem type detection (architect P2 finding) _primitives/_rust/keisei/src/fs_type.rs (NEW, 103 LOC) - statfs(2)-based detection on unix (libc = '0.2' under [target.'cfg(unix)'.dependencies]) - Recognizes exfat / msdos (FAT32) via f_fstypename on macOS, via f_type magic numbers on Linux (0x4d44, 0x2011bab0) - Windows stub returns Unknown (GetVolumeInformationW TBD) - warn_on_unsafe_fs(root) emits stderr warning on ExFat/Fat32 brain.rs::load calls warn_on_unsafe_fs after canonicalize+symlink checks. Warning NOT fatal — user can opt into single-client use. 2. Battle-test matrix (architect P3 finding) tests/battle/Dockerfile.install-test-alpine (NEW) - alpine:3.19 + apk rust/cargo/pandoc - Exposes musl-vs-glibc issues in aws-sdk-s3, rusqlite, git2 tests/battle/Dockerfile.install-test-debian (NEW) - debian:12 + rustup stable + pandoc - Default server distro, different apt structure from Ubuntu tests/battle/README.md rewritten — 3-distro matrix with run script 3. USB-BRAIN-GUIDE platform split docs/USB-BRAIN-GUIDE.md — restructured as TOC + platform-agnostic preamble + exFAT warning + cross-platform troubleshooting docs/USB-BRAIN-GUIDE-macos.md (NEW, 97 LOC) — Gatekeeper, diskutil, /Volumes, xattr -d com.apple.quarantine docs/USB-BRAIN-GUIDE-linux.md (NEW, 98 LOC) — /media/$USER, umount, ext4 recommended, systemd-udev auto-mount note docs/USB-BRAIN-GUIDE-windows.md (NEW, 115 LOC) — PowerShell Dismount-Volume, NTFS, FS-advisory Unknown caveat REAL VERIFICATION (paste from agent): cargo check -p keisei: Finished (clean) cargo test -p keisei --release: 32 passed 0 failed (30 existing + 2 new) docker buildx outline: both new Dockerfiles parse Constructor Pattern: fs_type.rs 103 LOC, brain.rs 198 LOC (at limit 200, held the line) All fns <30 LOC. Each USB guide sub-doc 97-115 LOC. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 3 + _primitives/_rust/Cargo.lock | 1 + _primitives/_rust/keisei/Cargo.toml | 3 + _primitives/_rust/keisei/src/brain.rs | 1 + _primitives/_rust/keisei/src/fs_type.rs | 103 ++++++ _primitives/_rust/keisei/src/main.rs | 1 + _primitives/_rust/keisei/tests/integration.rs | 40 +++ docs/USB-BRAIN-GUIDE-linux.md | 98 ++++++ docs/USB-BRAIN-GUIDE-macos.md | 97 +++++ docs/USB-BRAIN-GUIDE-windows.md | 115 ++++++ docs/USB-BRAIN-GUIDE.md | 331 +++--------------- tests/battle/Dockerfile.install-test-alpine | 42 +++ tests/battle/Dockerfile.install-test-debian | 46 +++ tests/battle/README.md | 56 ++- 14 files changed, 649 insertions(+), 288 deletions(-) create mode 100644 _primitives/_rust/keisei/src/fs_type.rs create mode 100644 docs/USB-BRAIN-GUIDE-linux.md create mode 100644 docs/USB-BRAIN-GUIDE-macos.md create mode 100644 docs/USB-BRAIN-GUIDE-windows.md create mode 100644 tests/battle/Dockerfile.install-test-alpine create mode 100644 tests/battle/Dockerfile.install-test-debian diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5bb6b..8c8b202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ _primitives/_rust/target/release/kei-changelog \ > ships must be replaced with the real commit summary before release. ### Added +- **primitives/keisei (v0.22 Track C — filesystem-type advisory):** new `fs_type.rs` cube (`<110 LOC`) classifies the brain root via `statfs(2)` on macOS + Linux and returns `FsWarning::{None,ExFat,Fat32,Unknown}`. Windows support deferred (returns `Unknown` until `GetVolumeInformationW` lands). `Brain::load` now prints a stderr advisory when exFAT / FAT32 is detected — SQLite WAL shared-mmap is unreliable there and `keisei mount` (multi-client) WILL corrupt `kei-memory` / `kei-artifact` / `kei-social-store` DBs. Warning is non-blocking — single-client `keisei attach` on exFAT stays supported. New runtime dep `libc = "0.2"` (unix-only). Two new integration tests (`brain_load_on_typical_filesystem_no_warn`, `fs_type_detection_returns_none_on_standard_fs`) — suite now 32/32 pass. +- **tests/battle (v0.22 Track C — distro matrix):** two new Dockerfiles alongside the existing `ubuntu:24.04` image. `Dockerfile.install-test-alpine` (Alpine 3.19 — musl libc, exposes musl-static-link quirks in `rusqlite` / `git2` / `aws-sdk-s3`). `Dockerfile.install-test-debian` (Debian 12 bookworm — glibc, different apt structure from Ubuntu). `README.md` documents the 3-image matrix and documents known musl-static-link failures as matrix signal rather than regression. +- **docs (v0.22 Track C — USB guide platform split):** `USB-BRAIN-GUIDE.md` restructured into a TOC + platform-agnostic preamble (prerequisites, exFAT/FAT32 warning, invariants, troubleshooting). Three new platform-specific walkthroughs: `USB-BRAIN-GUIDE-macos.md` (Gatekeeper `xattr`, `/Volumes/`, `diskutil`), `USB-BRAIN-GUIDE-linux.md` (`/media/$USER/`, `umount`, ext4, optional systemd-udev auto-attach), `USB-BRAIN-GUIDE-windows.md` (PowerShell, drive letter, NTFS, `Dismount-Volume`, FS-advisory returns `Unknown` caveat). - **primitives (v0.21 — keisei SSoT relocation + `Scope` enum):** - Marker file relocated from `~/.claude/keisei-attached.toml` to `~/.keisei/attached.toml`. `~/.claude/` is Claude-Code-specific territory and should not host cross-adapter keisei state. `config::read()` performs a one-shot migration the first time it runs under v0.21: if the legacy file exists and the new location is empty, the marker moves over (new file written, legacy file deleted) and a stderr notice is emitted. - `Scope` enum (`user` / `project`) on the `ClientAdapter` trait. Adapters declare `supported_scopes()`; `config_path(scope)`, `attach(brain, scope)`, `detach(brain_name, scope)` are scope-aware. Claude Code and Cursor support both scopes; Continue and Zed are user-only. `keisei attach` gains `--scope=` (default `user`); `keisei mount` stays host-wide (`Scope::User` fan-out by design). diff --git a/_primitives/_rust/Cargo.lock b/_primitives/_rust/Cargo.lock index ce2b5d8..a5950e2 100644 --- a/_primitives/_rust/Cargo.lock +++ b/_primitives/_rust/Cargo.lock @@ -1952,6 +1952,7 @@ name = "keisei" version = "0.1.0" dependencies = [ "clap", + "libc", "regex", "serde", "serde_json", diff --git a/_primitives/_rust/keisei/Cargo.toml b/_primitives/_rust/keisei/Cargo.toml index 00e9a1b..b7c6e83 100644 --- a/_primitives/_rust/keisei/Cargo.toml +++ b/_primitives/_rust/keisei/Cargo.toml @@ -19,5 +19,8 @@ thiserror = "2" regex = { workspace = true } tempfile = "3" +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [dev-dependencies] tempfile = "3" diff --git a/_primitives/_rust/keisei/src/brain.rs b/_primitives/_rust/keisei/src/brain.rs index 7a3fbe6..005b6b2 100644 --- a/_primitives/_rust/keisei/src/brain.rs +++ b/_primitives/_rust/keisei/src/brain.rs @@ -115,6 +115,7 @@ impl Brain { pub fn load(input: &Path) -> Result { v::reject_symlink_root(input)?; let root = v::canonicalize_root(input)?; + crate::fs_type::warn_on_unsafe_fs(&root); let manifest = v::read_manifest(&root)?; v::validate_schema(&manifest)?; v::validate_name(&manifest.brain.name)?; diff --git a/_primitives/_rust/keisei/src/fs_type.rs b/_primitives/_rust/keisei/src/fs_type.rs new file mode 100644 index 0000000..4b0e715 --- /dev/null +++ b/_primitives/_rust/keisei/src/fs_type.rs @@ -0,0 +1,103 @@ +//! Filesystem type detection for brain root. +//! +//! Warns when the brain sits on exFAT / FAT32, where SQLite WAL shared- +//! memory mmap (used by `kei-memory`, `kei-artifact`, `kei-social-store`) +//! is unreliable and `keisei mount` (multi-client) will corrupt DBs. +//! Single-client `keisei attach` stays supported, hence the warning is +//! advisory, never blocking. Platform calls: `statfs(2)` on macOS + +//! Linux; Windows returns `Unknown` until `GetVolumeInformationW` lands. + +use std::path::Path; + +#[derive(Debug, PartialEq, Eq)] +pub enum FsWarning { + None, + ExFat, + Fat32, + Unknown, +} + +/// Print a stderr advisory when the brain root lives on exFAT / FAT32. +/// Advisory only — load succeeds regardless. See [`detect_fs_warning`]. +pub fn warn_on_unsafe_fs(root: &Path) { + match detect_fs_warning(root) { + FsWarning::ExFat | FsWarning::Fat32 => { + eprintln!( + "[keisei] WARNING: brain root '{}' is on an exFAT/FAT32 filesystem. \ + SQLite WAL mode (used by kei-memory/artifact/social-store) is UNSAFE \ + on these filesystems. Use 'keisei attach' with ONE client at a time; \ + 'keisei mount' (multi-client fan-out) WILL corrupt memory DBs. \ + See docs/USB-BRAIN-GUIDE.md for recommended filesystems.", + root.display() + ); + } + _ => {} + } +} + +/// Classify the filesystem at `path`. NEVER returns `Result` — errors +/// collapse to `Unknown` so this stays call-safe inside `Brain::load`. +pub fn detect_fs_warning(path: &Path) -> FsWarning { + #[cfg(target_os = "macos")] + { + return macos_detect(path); + } + #[cfg(target_os = "linux")] + { + return linux_detect(path); + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + let _ = path; + FsWarning::Unknown + } +} + +#[cfg(target_os = "macos")] +fn macos_detect(path: &Path) -> FsWarning { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + let c_path = match CString::new(path.as_os_str().as_bytes()) { + Ok(s) => s, + Err(_) => return FsWarning::Unknown, + }; + // SAFETY: zero-init POD + valid CString ptr; statfs writes into buf. + let mut buf: libc::statfs = unsafe { std::mem::zeroed() }; + if unsafe { libc::statfs(c_path.as_ptr(), &mut buf) } != 0 { + return FsWarning::Unknown; + } + let name: String = buf + .f_fstypename + .iter() + .take_while(|b| **b != 0) + .map(|b| *b as u8 as char) + .collect(); + match name.as_str() { + "exfat" => FsWarning::ExFat, + "msdos" => FsWarning::Fat32, + _ => FsWarning::None, + } +} + +#[cfg(target_os = "linux")] +fn linux_detect(path: &Path) -> FsWarning { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + // man statfs(2) — magic numbers. + const MSDOS_SUPER_MAGIC: i64 = 0x4d44; + const EXFAT_SUPER_MAGIC: i64 = 0x2011_bab0; + let c_path = match CString::new(path.as_os_str().as_bytes()) { + Ok(s) => s, + Err(_) => return FsWarning::Unknown, + }; + // SAFETY: zero-init POD + valid CString ptr. + let mut buf: libc::statfs = unsafe { std::mem::zeroed() }; + if unsafe { libc::statfs(c_path.as_ptr(), &mut buf) } != 0 { + return FsWarning::Unknown; + } + match buf.f_type as i64 { + EXFAT_SUPER_MAGIC => FsWarning::ExFat, + MSDOS_SUPER_MAGIC => FsWarning::Fat32, + _ => FsWarning::None, + } +} diff --git a/_primitives/_rust/keisei/src/main.rs b/_primitives/_rust/keisei/src/main.rs index e29a74a..0b70b95 100644 --- a/_primitives/_rust/keisei/src/main.rs +++ b/_primitives/_rust/keisei/src/main.rs @@ -13,6 +13,7 @@ mod config; mod detach; mod display; mod error; +mod fs_type; mod fsx; mod list; mod mount; diff --git a/_primitives/_rust/keisei/tests/integration.rs b/_primitives/_rust/keisei/tests/integration.rs index 29c29f8..7be2b64 100644 --- a/_primitives/_rust/keisei/tests/integration.rs +++ b/_primitives/_rust/keisei/tests/integration.rs @@ -20,6 +20,8 @@ mod brain_validate; mod config; #[path = "../src/display.rs"] mod display; +#[path = "../src/fs_type.rs"] +mod fs_type; #[path = "../src/fsx.rs"] mod fsx; #[path = "../src/adapters/mod.rs"] @@ -993,3 +995,41 @@ fn mount_sanitizes_control_chars_in_error_reason() { ); } } + +// ----------------------------------------------------------------------- +// v0.22 Track C — filesystem type detection (fs_type.rs). +// ----------------------------------------------------------------------- + +#[test] +fn brain_load_on_typical_filesystem_no_warn() { + // On the dev / CI host the tmpdir sits on APFS (macOS) or ext4 + // (Linux) — neither of which should trigger the exFAT/FAT32 warn. + // We can't capture stderr from Brain::load easily, so we assert on + // the primitive that drives the advisory instead. `None` means "no + // warning would be emitted"; `Unknown` is the accepted fallback + // on platforms where statfs isn't wired. + let _g = setup_home(); + let brain_dir = tempfile::tempdir().unwrap(); + write_brain(brain_dir.path(), 1); + + let _b = brain::Brain::load(brain_dir.path()).expect("load succeeds on normal fs"); + + let w = fs_type::detect_fs_warning(brain_dir.path()); + assert!( + matches!(w, fs_type::FsWarning::None | fs_type::FsWarning::Unknown), + "standard tmpdir should not be classed as exFAT/FAT32, got {w:?}" + ); +} + +#[test] +fn fs_type_detection_returns_none_on_standard_fs() { + // Direct test of the primitive — no brain, no env mutation. + let td = tempfile::tempdir().unwrap(); + let w = fs_type::detect_fs_warning(td.path()); + // Must NEVER flag exFAT / FAT32 on a host tmpdir — the latter + // sits on APFS / ext4 / whatever the developer has locally. + assert!( + !matches!(w, fs_type::FsWarning::ExFat | fs_type::FsWarning::Fat32), + "detect_fs_warning misclassified tmpdir as {w:?}" + ); +} diff --git a/docs/USB-BRAIN-GUIDE-linux.md b/docs/USB-BRAIN-GUIDE-linux.md new file mode 100644 index 0000000..51606dc --- /dev/null +++ b/docs/USB-BRAIN-GUIDE-linux.md @@ -0,0 +1,98 @@ +# USB Exobrain — Linux Walkthrough + +> Platform-specific companion to `USB-BRAIN-GUIDE.md`. Read the top-level guide first for prerequisites, warnings, and invariants. + +On Linux, auto-mounted removable media typically lands at `/media/$USER/