Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bac1cdd
docs(uffs-mft): NTFS full-volume capture plan
githubrobbi Jul 5, 2026
56b09ce
feat(uffs-mft): sysinfo capture-host probe
githubrobbi Jul 5, 2026
5789997
feat(uffs-mft): NTFS metafile capture — $Boot (P1)
githubrobbi Jul 5, 2026
cc1328c
feat(uffs-mft): metafile capture — $Bitmap + generic stream reader (P…
githubrobbi Jul 5, 2026
193807d
feat(uffs-mft): metafile capture — $Secure:$SDS ACLs (P1.3)
githubrobbi Jul 5, 2026
2cad937
feat(uffs-mft): metafile capture — $AttrDef/$MFTMirr/$Volume/$BadClus…
githubrobbi Jul 5, 2026
40c38c7
feat(uffs-mft): capture orchestrator + manifest bundle (P1.5)
githubrobbi Jul 5, 2026
15d5b82
feat(uffs-mft): metafile capture — $LogFile (P2)
githubrobbi Jul 5, 2026
75571a8
feat(uffs-mft): metafile capture — $UsnJrnl:$J via $Extend traversal …
githubrobbi Jul 5, 2026
1186186
feat(uffs-mft): include compressed $MFT in the capture bundle
githubrobbi Jul 5, 2026
51ec34b
docs(uffs-mft): mark capture plan P0-P2/P4 shipped
githubrobbi Jul 5, 2026
725a812
feat(uffs-mft): offline metafile inspection — metafile-info + $Boot d…
githubrobbi Jul 5, 2026
7d44033
feat(uffs-mft): offline $Bitmap decode + split metafile_decode module…
githubrobbi Jul 5, 2026
cf34bf9
feat(uffs-mft): offline $UsnJrnl:$J change-journal decode (P6)
githubrobbi Jul 5, 2026
910a4b4
docs(uffs-mft): mark P6 offline decoders shipped
githubrobbi Jul 5, 2026
5c724f2
docs(uffs-mft): fix broken intra-doc links in metafile module doc
githubrobbi Jul 5, 2026
657a17f
fix(uffs-mft): sector-align metafile data-run reads ($Bitmap capture)
githubrobbi Jul 6, 2026
c97226a
fix(uffs-mft): follow $ATTRIBUTE_LIST to capture named $DATA streams
githubrobbi Jul 6, 2026
4e05801
fix(uffs-mft): populate NTFS major/minor version from extended volume…
githubrobbi Jul 6, 2026
8c2e76e
fix(uffs-mft): decode attribute names so named $DATA streams match
githubrobbi Jul 6, 2026
c1d107f
feat(uffs-mft): capture $UsnJrnl:$J via sparse-compact run reader
githubrobbi Jul 6, 2026
cf1fc81
fix(uffs-mft): reject garbage USN records in $UsnJrnl decoder
githubrobbi Jul 6, 2026
f9f8651
feat(uffs-mft): add capture --all-drives
githubrobbi Jul 6, 2026
ce39012
feat(uffs-mft): add capture --zip / --split-gib bundling
githubrobbi Jul 6, 2026
68ad3d7
feat(uffs-mft): add verify command for MFT CSV parity
githubrobbi Jul 6, 2026
70d043a
docs(uffs-mft): add MFT capture → transfer → verify runbook
githubrobbi Jul 6, 2026
942078a
fix(uffs-mft): name captured $MFT <DRIVE>_mft.bin for verify-flow compat
githubrobbi Jul 6, 2026
1e045d2
feat(scripts): add verify_parity --live --bundle to capture + zip tra…
githubrobbi Jul 6, 2026
d1834da
fix(scripts): verify_parity --bundle uses tar.zst, not Compress-Archive
githubrobbi Jul 6, 2026
151bd80
fix(uffs-mft): store real mtimes in capture --zip archives
githubrobbi Jul 6, 2026
134a41d
feat(scripts): verify_parity expands drive_<x>.tar.zst bundles offline
githubrobbi Jul 6, 2026
496fc71
fix(scripts): reset daemon before offline --regenerate (clean single-…
githubrobbi Jul 6, 2026
d33155b
fix(scripts): tolerate dir size-on-disk skew in live parity reconciler
githubrobbi Jul 6, 2026
b12b4be
fix(uffs-mft): write reserved_allocated.txt sidecar in capture bundle
githubrobbi Jul 6, 2026
256ae73
fix(uffs-cli): bound ps calls in --status so it can't hang
githubrobbi Jul 7, 2026
320e27e
feat(just): use-local stops daemon + MCP before rebuilding
githubrobbi Jul 7, 2026
fb5a18e
fix(uffs-mft): store reserved_allocated_bytes in .bin header (v3)
githubrobbi Jul 7, 2026
d1aad61
feat(uffs-mft): add extract-mft to pull the raw $MFT from a .bin
githubrobbi Jul 7, 2026
12fe3cb
chore(deps): bump crossbeam-epoch 0.9.18->0.9.20 (RUSTSEC-2026-0204)
githubrobbi Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ windows = { version = "0.62.2", features = [
"Win32_System_Memory",
"Win32_System_Pipes",
"Win32_System_ProcessStatus",
"Win32_System_Registry",
"Win32_System_Services",
"Win32_System_SystemInformation",
"Win32_System_Threading",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,180 +541,8 @@ fn mcp_http_json() -> serde_json::Value {
})
}

// ── MCP Stdio Sessions ───────────────────────────────────────────────────────

/// Print the MCP stdio session list (running `uffs --mcp run` processes).
#[expect(clippy::print_stdout, reason = "CLI user-facing output")]
fn print_mcp_stdio_section(palette: Palette) {
println!("{}", section(palette, "MCP Stdio Sessions"));
let sessions = find_mcp_stdio_processes();
if sessions.is_empty() {
println!(" {}", palette.dim("(none)"));
return;
}
let mut any_stale = false;
for (idx, session) in sessions.iter().enumerate() {
let parent = session
.parent_name
.as_deref()
.map_or(String::new(), |name| format!(" (parent: {name})"));
let glyph = if session.is_stale {
any_stale = true;
Glyph::Warn
} else {
Glyph::Up
};
let name = format!("{}. PID {}", idx + 1, session.pid);
let detail = format!(
"uptime {}{parent}",
uffs_client::format::format_duration(session.uptime)
);
println!("{}", status_row(palette, glyph, &name, &detail));
}
if any_stale {
println!(
" {}",
palette
.dim("Older binary; the AI host that launched them refreshes on its next start.")
);
}
}

/// JSON for the MCP stdio session list.
fn mcp_stdio_json() -> serde_json::Value {
let sessions: Vec<serde_json::Value> = find_mcp_stdio_processes()
.iter()
.map(|session| {
serde_json::json!({
"pid": session.pid,
"uptime_secs": session.uptime.as_secs(),
"parent": session.parent_name,
"stale": session.is_stale,
})
})
.collect();
serde_json::Value::Array(sessions)
}

/// Information about a running MCP stdio process.
struct StdioSession {
/// Process ID.
pid: u32,
/// How long the process has been running.
uptime: core::time::Duration,
/// Name of the parent process (the AI host), if available.
parent_name: Option<String>,
/// True if the process's binary is older than the current binary.
is_stale: bool,
}

/// Find running `uffs --mcp run` processes via `ps`, flagging stale binaries.
fn find_mcp_stdio_processes() -> Vec<StdioSession> {
let Ok(raw_output) = std::process::Command::new("ps")
.args(["-eo", "pid,ppid,etime,args"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
else {
return Vec::new();
};
// AUDIT-OK(bytes): per-line PID scan of process-list output; each line's
// PID parses-or-skips (fail-safe). Whole-buffer strict decode would drop
// the whole list on one bad byte. (WI-4.3 follow-up)
let text = String::from_utf8_lossy(&raw_output.stdout);
let my_pid = std::process::id();
let current_mtime = std::env::current_exe()
.ok()
.and_then(|path| std::fs::metadata(path).ok())
.and_then(|meta| meta.modified().ok());

let mut sessions = Vec::new();
for line in text.lines().skip(1) {
if let Some(session) = parse_stdio_line(line, my_pid, current_mtime) {
sessions.push(session);
}
}
sessions
}

/// Parse one `ps` line into a [`StdioSession`] if it is an MCP stdio process.
fn parse_stdio_line(
line: &str,
my_pid: u32,
current_mtime: Option<std::time::SystemTime>,
) -> Option<StdioSession> {
let mut fields = line.split_whitespace();
let proc_pid = fields.next()?.parse::<u32>().ok()?;
if proc_pid == my_pid {
return None;
}
let parent_pid: u32 = fields.next().and_then(|val| val.parse().ok()).unwrap_or(0);
let elapsed_time = fields.next()?;
let cmdline: String = fields.collect::<Vec<_>>().join(" ");

if !cmdline.contains("mcp") || !cmdline.contains("run") {
return None;
}
if cmdline.contains("serve") || cmdline.contains("start") || cmdline.contains("kill") {
return None;
}

let uptime = parse_ps_etime(elapsed_time);
let is_stale = current_mtime.is_some_and(|bin_mtime| {
let proc_started = std::time::SystemTime::now() - uptime;
proc_started < bin_mtime
});
Some(StdioSession {
pid: proc_pid,
uptime,
parent_name: resolve_parent_name(parent_pid),
is_stale,
})
}

/// Parse `ps` elapsed time format: `[[dd-]hh:]mm:ss`.
fn parse_ps_etime(etime: &str) -> core::time::Duration {
let mut total_secs: u64 = 0;
let (days_part, time_part) = if let Some((days, rest)) = etime.split_once('-') {
(days.parse::<u64>().unwrap_or(0), rest)
} else {
(0, etime)
};
total_secs += days_part * 86400;
let mut parts = time_part.rsplit(':');
if let Some(sec) = parts.next() {
total_secs += sec.parse::<u64>().unwrap_or(0);
}
if let Some(min) = parts.next() {
total_secs += min.parse::<u64>().unwrap_or(0) * 60;
}
if let Some(hour) = parts.next() {
total_secs += hour.parse::<u64>().unwrap_or(0) * 3600;
}
core::time::Duration::from_secs(total_secs)
}

/// Resolve the name of a parent process by PID.
fn resolve_parent_name(ppid: u32) -> Option<String> {
if ppid == 0 {
return None;
}
let output = std::process::Command::new("ps")
.args(["-p", &ppid.to_string(), "-o", "comm="])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
// Strict decode: this process name is returned and used for a
// comparison/targeting decision, so invalid UTF-8 fails closed (None)
// rather than yielding a U+FFFD-mangled name. (WI-4.3 follow-up)
let name = core::str::from_utf8(&output.stdout).ok()?.trim().to_owned();
if name.is_empty() {
return None;
}
let short = name.rsplit('/').next().unwrap_or(&name).to_owned();
Some(short)
}
mod process_scan;
use process_scan::{mcp_stdio_json, print_mcp_stdio_section};

// ── small shared helpers ─────────────────────────────────────────────────────

Expand Down
Loading
Loading