feat(minimal): add min bug diagnostic bundle command - #784
Conversation
📝 WalkthroughWalkthroughThe PR adds the ChangesDiagnostic bundle foundations
Diagnostic collection flow
Logging lifecycle
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant Minimal as min bug
participant Provider as Provider socket
participant Daemon as minimald
participant Archive as BundleWriter
User->>Minimal: run min bug
Minimal->>Archive: create host bundle
Minimal->>Provider: probe socket
Provider-->>Minimal: probe stages and client
Minimal->>Daemon: request diagnostic subsystem
Daemon-->>Minimal: stream daemon tar.zst
Minimal->>Archive: add host and guest entries
Archive-->>User: finalized bundle and manifest
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
4d4cfc8 to
b3850fb
Compare
Design note: hand-rolled collectors vs. public diagnostic/scraping cratesA fair review question for this PR is why the collectors are hand-rolled reads instead of an off-the-shelf system-info crate. Summary of the reasoning and the alternatives we could have reached for: Dependency footprintThis PR adds zero new external crates. The collectors are Why raw reads instead of a scraping library
Crates we could have used, per need
Bottom lineThe collection layer is ~2.5k lines including tests, most of it best-effort file reads where raw fidelity and failure isolation matter more than typed models. The redaction engine is where the real logic lives, and it had to be bespoke. Worth re-evaluating |
|
Field notes from a live incident this PR is squarely aimed at (#788 — an established attach stream died silently mid-session on the unstable release build, which has none of this diag plumbing). The bundle as designed would have fixed the two biggest gaps we hit (no persisted daemon logs, no in-VM state capture). Three additions the incident argues for:
🤖 Generated with Claude Code |
|
All three field-note items from the incident notes are now on the branch, plus host network capture:
🤖 Generated with Claude Code |
…ostics Key-based secret masking (redact_json / is_sensitive_key, with wholesale masking of vars/env-style tables) and a metadata-only recursive directory listing. Shared by the upcoming `min bug` CLI collector and the minimald diagnostic RPC so both sides apply identical redaction rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New streaming RPC: the client writes one JSON DiagBundleRequest and half-closes; the daemon streams back a zstd-compressed tar of its diagnostic bundle. Pre-stream errors relay over extended-data stream 1, mirroring WorkspaceFilesTarZst. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A detached supervisor's stdout goes nowhere, so in-field boot failures left no log trail. `run --detach` now marks its re-exec'd child with MINVMD_DETACHED, which routes tracing to <state>/logs/minvmd.log.<date> (14 retained), mirroring minimald's scheme so diagnostics find both daemons' logs in one place. Tracing init moves after CLI parsing because the log dir derives from the --minimal-state-dir override; the run.log stderr redirect stays and keeps catching panics and the final error print. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New diag module streams the daemon's own view for `min bug`: meta (version, pid, in_microvm, state_volume_mounted), rolling logs (tail-capped), state-dir listing (metadata only), session records with secret-shaped values redacted, /proc process and net tables, a gvproxy gateway probe when in a microVM, and disk/mounts. Collector failures accumulate into an errors.json entry instead of aborting, so a half-broken daemon still reports what it can. Served identically by the native daemon and the in-VM pid-1 instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One command for in-field debugging: writes minimal-diag-<timestamp>.tar.zst containing host system/process/network state, redacted config and loadouts, state-dir listings (never session file contents), rolling daemon logs, and — per provider instance — a staged socket probe (stat -> connect -> handshake -> GetVersion, whose failing stage distinguishes stale-socket from wedged-vsock from healthy) plus the daemon's own bundle streamed over the new DiagBundleTarZst subsystem. Every collector is failure-isolated into manifest.json errors; the command never autospawns daemons and never mutates state (reads minvmd.toml raw rather than via effective_state(), which repairs on read). client.key and host keys are never opened. The guest download is bounded by --guest-timeout-secs (60s default) and a 256 MiB cap; `min dirs` now names the minvmd rolling log too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The microVM's pid-1 minimald logged only to stdout (serial console), so `min bug`'s guest bundle never carried the in-VM daemon's logs: the `logs` collector reads <state>/logs, which stayed empty. Two causes stacked — pid-1 is never `--detach`'d (so it took the stdout branch), and init_tracing runs before the state volume mounts (so a naive file path would be shadowed by the later mount). Route the microVM to log to both the console (kept for boot diagnosis via the host boot.log) and a daily-rotated <state>/logs/minimald.log. A DeferredFileWriter is installed at init (discarding) and pointed at the appender once /var/lib/minimal is mounted and state has relocated onto it. Activation failure only warns and falls back to console — it must never wedge pid-1. Verified on macOS via `just dm1`: the guest bundle now contains logs/minimald.log.<date> with real runtime records, errors.json == []. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
download_diag_bundle only matched Data and ExtendedData in its channel
loop, so when a daemon that predates the RPC refuses the
DiagBundleTarZst subsystem, the resulting SSH_MSG_CHANNEL_FAILURE fell
into `_ => {}` and was dropped. A bare refusal does not close the
channel, so wait() never returned None and the download blocked until
the caller's guest timeout (60s per provider).
Handle ChannelMsg::Failure: bail immediately with an upgrade hint. The
happy path is unchanged — a healthy daemon replies Success and then
streams the bundle.
Verified: against a pre-diagnostics daemon `min bug` now errors in
0.08s (was 60s) with a clear message; against a current daemon the
bundle still downloads with errors[] empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two review findings let secret-shaped values escape the `min bug` redaction pass: - A sensitive-named key holding a table/array (`[tokens]`, `api_token = [...]`) recursed without the mask, leaking its members unless their own keys also looked sensitive. The sensitive-key check now propagates into container values, mirroring env-table semantics. - Env vars matching the project prefixes were captured verbatim even when their names were secret-shaped (`MINIMAL_AUTH_TOKEN`). The allowlist now defers to the sensitive-name deny, and the bare `MINIMAL` prefix is anchored to `MINIMAL_`. Also anchors the `public` exemption to `public_key` shapes so `publication_secret` and friends stay masked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on the DiagBundleTarZst server path: - If the channel copy failed mid-stream (client disconnect), the build task stayed blocked writing into the full duplex pipe forever, leaking the task and its ServerStateHandle clone. The pipe's read half is now dropped before the task is awaited so its next write fails with BrokenPipe, and copy errors propagate first. - The logs collector bundled the newest five files of any name from the shared logs dir; on native installs minvmd's rotated logs sort after minimald's and crowded them out entirely. It now takes only `minimald.log*` files — minvmd's logs belong to the host collector. - Collectors used blocking std::fs on the RPC-serving runtime; reads now go through tokio::fs and the /proc walk runs in spawn_blocking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PROCESS_MARKERS were substring-matched against the whole ps args column, so a user editing or tailing our logs (`vim minimald.log`, `tail -f minvmd.log.2026-07-15`) had their process — full command line, plus /proc status on Linux — captured into the bundle. Only the executable name is matched now, the same discipline the `min`/`minimal` names already used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on the guest download path of `min bug`: - A daemon error arriving after some archive bytes had streamed was silently discarded, landing a corrupt nested archive with a clean manifest. Any daemon-reported error now fails the download, and the guest collector records it in the bundle as before. - A truncated guest bundle was recorded both as skipped and as collected-verbatim; it is now a single collected entry with a new `truncated` redaction level. - Guest collector errors were timed from the start of the whole run instead of the provider's own collection. - Host collectors used blocking std::fs in async context; they now use tokio::fs (provider_dirs became async accordingly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The in-VM daemon's rolling log file (persisted to the state volume for `min bug`) holds a write-open fd under /var/lib/minimal at shutdown. That fd defeats both the plain unmount and the read-only remount in quiesce_state_volume, so every clean stop left the ext4 journal dirty (INCOMPAT_RECOVER set) — caught by stop_quiesces_volume_leaving_clean_ext4_journal in CI. pid-1 now parks the appender handle and its worker guard in a release hook next to the quiesce code; the Shutdown RPC runs it (idempotently) after the session drain and before the quiesce, flushing and closing the file. Console logging (serial -> host boot.log) is unaffected, so the teardown records still land in the host's boot log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Own-IP guest egress rides 100.64/16, inside the CGNAT block that Tailscale-style VPNs also occupy; published-port and MTU tickets need to know which host addresses and default route exist. Neither was visible in the bundle. host/net/interfaces.txt (`ip addr` / `ifconfig -a`) with every MAC masked to its vendor OUI — the IPs and subnets are the diagnostic payload, hardware identifiers are not — and host/net/routes.txt (`ip route` / `netstat -rn`) verbatim. Same attempt-list-with-timeout pattern as the listening-sockets collector; hosts without the tools degrade to a recorded collector error, which the integration test now accepts as the alternative to the files being present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hvc0 console was captured only when the dev-only MINVMD_BOOT_LOG env var was set; detached production boots discarded it, which is exactly the evidence lost when a guest wedges before the daemon is reachable or its transport dies afterwards (#788). minvmd now defaults the console capture to <provider dir>/boot.log, truncated per boot so it holds the current VM generation; the env var still overrides, and a capture failure logs a warning and boots on — diagnostics must never become a boot dependency. `min bug` bundles the file per provider (tail-capped) alongside run.log. Refs: #788 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On guest-bundle timeout or download failure the bundle carried only an error.txt — yet the failure domain in that case is often the vsock transport itself, the very path DiagBundleTarZst rides, so the guest's logs were lost precisely when they mattered most (#788). The data volume is plain ext4 and safe to read read-only from the host while the VM runs. On any guest-collection failure (including a dead socket probe) `min bug` now records the image's vital signs (volume-meta.json — its mtime alone dates a stall) and best-effort harvests /logs from the image via `debugfs -c` into providers/<n>/guest/volume-logs/ when e2fsprogs is installed; absent that, the manifest carries the exact offline-extraction command. Refs: #788 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"It's frozen" diagnosis runs on thread stacks and socket states of the min/minvmd/VMM family (#788: vCPUs parked in WFI, proxy in kevent, a unix socket open with no EOF) — none of which the bundle captured. For up to eight matched pids: a 1-second `sample` per process on macOS (host/proc/<pid>.sample.txt); wait channel, current syscall, kernel stack (root-only — the error is data) and readlink'd fds on Linux (host/proc/<pid>.stack.txt); plus one `lsof -nP` over the whole set (host/proc/lsof.txt), accepting lsof's exit-1-with-output convention. Every miss degrades to a manifest note. Refs: #788 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9781751 to
19a1053
Compare
minvmd now defaults the hvc0 console capture to <provider>/boot.log, so the blanket exports in `just env`/dm1/dm3 did nothing but redirect the console AWAY from the path the `min bug` collector reads — dev bundles silently lost their boot log while production installs kept theirs. Drop the exports (and the scratch boot.log from `just clean`); the env var stays supported for callers that set it deliberately (bench/soak scripts, CI). session-e2e's failure diagnostics now tail the provider-dir default when the variable is unset, instead of a knowingly-dead path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 20
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (2)
crates/common/src/listing.rs-12-18 (1)
12-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winOnly emit the truncation marker when an entry was omitted.
At Line 16, an exactly
max_entries-sized tree is incorrectly reported as truncated. Track whether traversal actually encountered another entry after reaching the cap, and add an exact-cap test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/common/src/listing.rs` around lines 12 - 18, Update listing_text and the walk_listing traversal to distinguish reaching max_entries from actually encountering an omitted entry; emit the truncation marker only when traversal confirms an additional entry exists beyond the cap. Add a test covering a tree with exactly max_entries entries and verify it is not marked truncated.crates/common/src/listing.rs-41-68 (1)
41-68: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReport symlink metadata without dereferencing.
entry.metadata()follows symlinks, so symlink entries are printed as their target’s type/size/mode even though recursion already skips them. Switch this tosymlink_metadata()and add a symlink case to the test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/common/src/listing.rs` around lines 41 - 68, Update the metadata lookup in walk_listing to use symlink_metadata() so symlink entries are reported using their own type, size, and mode without dereferencing; keep the existing file_kind handling and recursion behavior unchanged. Add or update the relevant listing test to cover a symlink and verify it is reported as a symlink rather than its target.
🧹 Nitpick comments (1)
crates/minimal/tests/bug.rs (1)
249-265: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winVerify that
--no-guestmakes no daemon connection.This only proves that the downloaded archive is omitted; it still passes when
cmd_bugprobes and handshakes with the daemon. Instrument the test server and assert that no handshake or diagnostic RPC is received.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/minimal/tests/bug.rs` around lines 249 - 265, Update bug_no_guest_skips_daemon_contact to instrument the test daemon from setup, then assert that it receives neither a handshake nor a diagnostic RPC when cmd_bug runs with --no-guest. Retain the existing archive assertions and use the daemon’s established request or call-tracking mechanism.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/common/src/redact.rs`:
- Around line 35-40: Update is_sensitive_key so explicit secret markers take
precedence over the public-key exemption: classify keys containing sensitive
markers such as token, password, or private as sensitive even when they match
public-key patterns. Restrict the false return to unambiguously
public-key-shaped names, and add regression tests covering compound keys
including public_key_token, public_key_password, and private_public_key.
In `@crates/minimal/src/client.rs`:
- Around line 260-275: Bound daemon_error accumulation in the channel.wait loop
alongside bundle in the ExtendedData ext: 1 branch. Limit appended bytes to the
remaining max_bytes capacity, mark truncation or stop reading once the
diagnostic limit is reached, and preserve the existing archive-data handling.
In `@crates/minimal/src/diag/bundle.rs`:
- Around line 72-74: Update the file-collection logic around
tokio::fs::File::open to open sources with O_NOFOLLOW, reject anything that is
not a regular file, and record rejected or symlink entries as skipped rather
than failing or copying them. Preserve the existing context for genuine open
errors and continue bundling other files.
- Around line 34-39: Update create to ensure the diagnostic archive at out_path
has owner-only permissions (0600), including when overwriting an existing file.
Preserve the existing context-wrapped file creation and configure or apply the
permissions after creation so the final mode is not affected by the process
umask.
- Around line 75-96: Update the file-reading flow around the metadata, seek, and
read_to_end calls to enforce cap on the reader itself, using a bounded read such
as take(cap) after positioning at the tail when needed. Ensure read_to_end
cannot consume more than cap bytes even if the file grows during reading, while
preserving the existing TailCapped and None redaction behavior in add_bytes.
In `@crates/minimal/src/diag/collect.rs`:
- Around line 380-389: Update the file filter in the rolling-log collection loop
for prefixes minimald.log and minvmd.log to accept only filenames matching the
exact prefix.YYYY-MM-DD pattern. Reject arbitrary suffixes such as
.private-notes before files are opened, while preserving the existing
newest-first sorting and collection behavior for valid dated logs.
- Around line 193-205: Update env to use std::env::vars_os() instead of
std::env::vars(), preventing invalid Unicode environment entries from aborting
collection. Convert names safely for allowlist checks and JSON keys, preserve
value lengths using OsStr/OsString byte or encoded representations, and retain
the existing redaction behavior and host/env.json output through BundleWriter.
In `@crates/minimal/src/diag/guest.rs`:
- Around line 128-135: Update the debugfs Command construction in the timeout
flow to enable kill_on_drop(true), ensuring the child process is terminated when
timeout drops the output future. Keep the existing timeout duration, arguments,
image path, and output handling unchanged.
In `@crates/minimal/src/diag/mod.rs`:
- Around line 113-151: Update the collection flow around net::probe_socket and
the guest_result match so args.no_guest is checked before probing the daemon.
When --no-guest is enabled, record the guest collection as skipped and avoid
calling probe_socket or performing any daemon handshake; otherwise preserve the
existing socket probe and guest collection behavior.
In `@crates/minimal/src/diag/net.rs`:
- Around line 135-142: Update command_output to configure the spawned tokio
process with kill_on_drop(true) before applying the timeout, ensuring
netstat/ifconfig children terminate when the timeout drops the output future.
- Around line 240-247: Update the Unix socket connection in the Stage::run call
to enforce the probe’s per-stage deadline before recording the result in
probe.connect. Apply the timeout to the awaited UnixStream::connect future,
preserve the existing success mapping, and convert both connection and timeout
failures into the expected error string format.
In `@crates/minimal/src/diag/procs.rs`:
- Around line 198-214: Update the process-list formatting in the relevant
function, including the analogous path around the additional reported range, so
command-line output preserves argv0 and process metrics but does not emit
complete arguments. Redact or omit subsequent arguments and sensitive flag
values before appending lines to text, while keeping the existing PID filtering
and collection behavior unchanged.
- Around line 126-148: The diagnostic capture paths around command_capture and
the /proc/<pid>/fd walk need bounded resource usage and child cleanup. Limit
captured command output by bytes, cap the number of file descriptors traversed,
and ensure timeout handling explicitly kills and reaps the spawned child rather
than only dropping its future. Apply these limits consistently to the
sample/lsof capture callers while preserving existing error reporting.
In `@crates/minimal/src/diag/redact.rs`:
- Around line 11-25: Replace the broad project prefixes in
ENV_VALUE_ALLOWLIST_PREFIXES with an explicit list of known-safe project
variable names, while retaining the exact allowlist and the sensitive-key
rejection in is_env_value_allowlisted. Ensure variables such as
MINIMAL_DATABASE_URL and MINIMAL_WEBHOOK_URL are no longer allowlisted unless
explicitly known to be safe.
In `@crates/minimald/src/diag.rs`:
- Around line 139-143: Update the include_state_listing branch to execute
common::listing::listing_text through the blocking pool, awaiting its result
before calling append_bytes. Preserve the existing LISTING_MAX_ENTRIES limit and
state-listing error reporting, including handling any blocking-task failure.
- Around line 303-333: Update proc_table to stop reading and exporting raw
/proc/<pid>/cmdline contents. Replace the cmdline value with a safe process
identity such as /proc/<pid>/comm or the executable basename, ensuring the
generated table never exposes command-line arguments or secrets.
- Around line 257-284: Update the session-record loop around
read(entry.path().join("record.json")) and serde_json::from_slice so unreadable
or malformed records add a per-record collector error or safe errors.json entry
before continuing. Include the session identifier and failure context without
exposing record contents, while preserving processing of subsequent sessions.
- Around line 217-235: Update the log-entry collection loop before pushing paths
into files: validate each entry as a regular, non-symlink file and ensure its
filename matches the expected minimald.log rolling-name shape. Skip symlinks,
directories, and other non-regular entries so read_tail() only processes
eligible log files.
- Around line 45-55: Bound diagnostic request handling in stream_diag_bundle and
the corresponding request-read paths: apply a short timeout to read_to_end,
enforce a small maximum request-body size before parsing, and validate or cap
the requested log_tail_bytes against a server-defined maximum before allocating
or collecting logs. Preserve normal parsing and response behavior for requests
within all limits.
In `@crates/minvmd/src/cmd/vmm_child.rs`:
- Around line 89-95: Update the console registration flow around
ctx.set_console_output so any failure to register the boot log is logged as a
warning and does not propagate from run_vmm. Preserve the existing best-effort
File::create handling and continue VM boot regardless of either diagnostic setup
failure.
---
Minor comments:
In `@crates/common/src/listing.rs`:
- Around line 12-18: Update listing_text and the walk_listing traversal to
distinguish reaching max_entries from actually encountering an omitted entry;
emit the truncation marker only when traversal confirms an additional entry
exists beyond the cap. Add a test covering a tree with exactly max_entries
entries and verify it is not marked truncated.
- Around line 41-68: Update the metadata lookup in walk_listing to use
symlink_metadata() so symlink entries are reported using their own type, size,
and mode without dereferencing; keep the existing file_kind handling and
recursion behavior unchanged. Add or update the relevant listing test to cover a
symlink and verify it is reported as a symlink rather than its target.
---
Nitpick comments:
In `@crates/minimal/tests/bug.rs`:
- Around line 249-265: Update bug_no_guest_skips_daemon_contact to instrument
the test daemon from setup, then assert that it receives neither a handshake nor
a diagnostic RPC when cmd_bug runs with --no-guest. Retain the existing archive
assertions and use the daemon’s established request or call-tracking mechanism.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 002f733c-a8b6-43ab-8134-e9667cfc9608
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
crates/common/src/lib.rscrates/common/src/listing.rscrates/common/src/redact.rscrates/minimal/Cargo.tomlcrates/minimal/src/client.rscrates/minimal/src/diag/bundle.rscrates/minimal/src/diag/collect.rscrates/minimal/src/diag/guest.rscrates/minimal/src/diag/manifest.rscrates/minimal/src/diag/mod.rscrates/minimal/src/diag/net.rscrates/minimal/src/diag/procs.rscrates/minimal/src/diag/redact.rscrates/minimal/src/dirs.rscrates/minimal/src/lib.rscrates/minimal/tests/bug.rscrates/minimal/tests/common/mod.rscrates/minimald-rpc/src/lib.rscrates/minimald/src/diag.rscrates/minimald/src/guest.rscrates/minimald/src/lib.rscrates/minimald/src/main.rscrates/minimald/src/rpc.rscrates/minimald/src/server.rscrates/minvmd/Cargo.tomlcrates/minvmd/src/cmd/run.rscrates/minvmd/src/cmd/vmm_child.rscrates/minvmd/src/lib.rscrates/minvmd/src/main.rsjustfilescripts/session-e2e.sh
| let mut file = tokio::fs::File::open(src) | ||
| .await | ||
| .with_context(|| format!("opening {}", src.display()))?; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not follow symlinks when collecting files.
File::open follows symlinks, so a crafted log entry can make the bundle copy an unrelated readable file. Open with O_NOFOLLOW, verify it is a regular file, and record other entries as skipped.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/minimal/src/diag/bundle.rs` around lines 72 - 74, Update the
file-collection logic around tokio::fs::File::open to open sources with
O_NOFOLLOW, reject anything that is not a regular file, and record rejected or
symlink entries as skipped rather than failing or copying them. Preserve the
existing context for genuine open errors and continue bundling other files.
There was a problem hiding this comment.
Partially addressed: add_file_tail now lstat-rejects non-regular files (symlink_metadata, no follow), which closes the exfiltration-via-shared-bundle path. Full O_NOFOLLOW plumbing is unnecessary — no privilege boundary is crossed (same-user), and the residual risk was the planted-symlink case this guard covers.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline crates/minimal/src/diag/bundle.rs --items all --type function,method --match 'add_file_tail|add_bytes|append'
printf '\n--- add_file_tail implementation and nearby tests ---\n'
nl -ba crates/minimal/src/diag/bundle.rs | sed -n '45,150p'
printf '\n--- relevant file-operation references in the diagnostic module ---\n'
rg -n -C 3 'symlink_metadata|File::open|O_NOFOLLOW|is_file|file_type' crates/minimal/src/diagLength of output: 1092
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- add_file_tail ---'
sed -n '75,135p' crates/minimal/src/diag/bundle.rs
printf '%s\n' '--- tests ---'
sed -n '250,330p' crates/minimal/src/diag/bundle.rs
printf '%s\n' '--- related diagnostic file operations ---'
rg -n -C 3 'symlink_metadata|File::open|O_NOFOLLOW|is_file|file_type' crates/minimal/src/diagLength of output: 7794
@norrietaylor, thanks—the symlink_metadata guard correctly rejects symlinks already present when checked, so it is a meaningful improvement. However, I can’t mark this resolved: there is still a TOCTOU window between symlink_metadata (Line 92) and File::open (Line 98). A process able to modify the source path can replace the regular file with a symlink in that interval, and File::open will follow it.
This remains relevant even without a privilege boundary: the impact is including another readable local file in a diagnostic archive that may be shared externally. Opening with O_NOFOLLOW (and validating the opened descriptor is a regular file) closes that remaining race.
✏️ Learnings added
Learnt from: norrietaylor
Repo: gominimal/minimal PR: 784
File: crates/minimal/src/diag/bundle.rs:98-100
Timestamp: 2026-07-17T00:15:22.192Z
Learning: For `crates/minimal/src/diag/bundle.rs`, the maintainer considers `symlink_metadata`-based non-regular-file rejection sufficient in the same-user threat model and views additional `O_NOFOLLOW` plumbing as unnecessary. The implementation still uses `tokio::fs::File::open` after the metadata check.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
The #788 analysis was derailed twice by log framing: abrupt closes of the CLI's own oneshot RPC connections logged as WARN "session ended with error" (reading like transport churn), and the binding's duplicate-request skips logged as WARN with no channel identity (reading like a dropped shell request). - every accepted connection gets a monotonic `conn` id, stamped on accept, handshake failure, and close - peer hangups (early eof / broken pipe / disconnected) close at INFO as "connection closed by peer"; real errors stay WARN - bindings log attach and mainloop exit (with reason) at INFO, carrying their channel id — the missing correlation for adoption-stall triage - the catch-all for pre-attach request duplicates drops to DEBUG with the channel id Refs: #788 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Suspend/resume is the most plausible mundane explanation for a long-lived stream dying silently (#788), yet ruling it in or out meant asking the user to run pmset by hand. host/power.txt now captures the last 100 sleep/wake transitions (pmset -g log on macOS, plus sysctl kern.{boottime,sleeptime,waketime}; journalctl -b -k suspend/resume lines on Linux), best-effort in the collector idiom. Today's incident was exonerated this way — the host stayed awake through the wedge — and that check is now one bundle away instead of a support round-trip. Refs: #788 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| /// The staged probe record for one provider socket. | ||
| #[derive(Serialize)] | ||
| pub struct SocketProbe { |
There was a problem hiding this comment.
What is a socket probe?
There was a problem hiding this comment.
The socket probe is the staged connection-health record for one provider.
It walks the four steps of reaching the daemon in order — stat() the socket file → connect() → SSH handshake+auth → GetVersion RPC — and records which step each one reached. The failing step is the
diagnosis:
- no socket file → daemon never ran or cleaned up;
- connect refused → stale socket after a crash;
- handshake timeout → wedged guest behind libkrun's always-accepting bridge;
- RPC failure → daemon up but unhealthy.
| /// stop. The binary registers a closure here after wiring the appender; | ||
| /// [`release_volume_log`] runs it (once) so the file is closed before the | ||
| /// unmount. Console logging is unaffected. | ||
| static VOLUME_LOG_RELEASE: std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>> = |
There was a problem hiding this comment.
Static global mutexes are a smell and rather unsafe in the context of forks - what does this do and can we do it more idiomatically?
There was a problem hiding this comment.
Everyone agrees this is a smell 👃
| /// is mounted, via [`DeferredFileWriter::activate`]. | ||
| #[derive(Clone, Default)] | ||
| struct DeferredFileWriter { | ||
| inner: std::sync::Arc<std::sync::RwLock<Option<tracing_appender::non_blocking::NonBlocking>>>, |
There was a problem hiding this comment.
std::sync::Arc<std::sync::RwLock<Option<tracing_appender::non_blocking::NonBlocking>>>
Wat
| // The on-volume log appender holds a write-open fd under the mountpoint, | ||
| // which defeats both the clean unmount and the read-only remount — | ||
| // close it first. Records keep flowing to the console (host boot.log). | ||
| crate::guest::release_volume_log(); |
There was a problem hiding this comment.
Function calls to change state dont scale, and this is part of that global static mutex being a smell. Maybe a VolumeLog type that mediates log writing, and its Drop does the release?
Are we really sure theres no existing crates that do logging + rotation?
There was a problem hiding this comment.
Good question: I looked for collection and scrubbing crates but didn't look for log rotation (which sure seems like a solved problem)
| // `accepted connection`/`closed` lines in `minimald::server` — the | ||
| // #788 analysis stalled twice for want of this correlation. | ||
| let chan_id = self.channel.id(); | ||
| tracing::info!(channel = %chan_id, "binding attached to session channel"); |
There was a problem hiding this comment.
The other way to do this would be to setup a tracing span (wired for async with tracing::Instrument) and have this as metadata.
| } | ||
| // Mark the child as detached so it routes tracing to the rolling | ||
| // log file (`<state>/logs/minvmd.log.<date>`) instead of stdout. | ||
| cmd.env(crate::DETACHED_ENV, "1"); |
There was a problem hiding this comment.
Why dont we just do this unconditionally? also we can do both log files and stdout in the tracing init config
| .build(&log_dir) | ||
| .context("building rolling log appender")?; | ||
| let (writer, guard) = tracing_appender::non_blocking(appender); | ||
| tracing_subscriber::registry() |
There was a problem hiding this comment.
Wont this overwrite the one init'd when DETACHED_ENV is set?
There was a problem hiding this comment.
The file init here runs only in the detached case and the stdout init only in the foreground case,
- one .init() per process
- init_tracing early-returns: if DETACHED_ENV.is_none()
- run --detach re-execs minvmd run with DETACHED_ENV=1,
- foreground parent and the detached child are separate processes
Hardening from CodeRabbit review of the min bug diagnostics work. common: - is_sensitive_key: strip the exempt `public_key` token before the marker scan so `public_key_token` / `private_public_key` stay masked (fail-closed); regression tests for the compound keys minimal: - bundle file created 0600 (unredacted contents, lands in cwd) - add_file_tail: lstat-reject symlinks and bound the read with take(cap) so a live-appended log can't exceed the cap - env collector uses vars_os (vars panics on non-UTF-8, unwinding past the collect_step! guard and aborting the whole run) - download_diag_bundle caps the accumulated daemon error at 64 KiB - --no-guest skips the socket probe too (it handshakes with the daemon, which the flag promises not to do) - kill_on_drop on every timed subprocess (net, procs, guest debugfs, system uname) minimald: - proc table records full argv only for the minimal process family; everything else contributes comm (name) only — a native daemon's /proc is the whole host process table - state listing runs on spawn_blocking, off the RPC-serving runtime - session records: read failures and unparseable-withheld records leave a record.error.txt note instead of vanishing - log collector rejects non-regular files (symlink on a guest-writable volume) minvmd: - boot-log console wiring failure warns and boots on, matching its own "not a boot dependency" comment (was propagating) Refs: #784 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maintainer review: common is for baseline types used everywhere, and
the diagnostics machinery doesn't belong there. Move the app-agnostic
core into a new `diagnostics` crate:
- redact (was common::redact) — key-based secret masking
- listing (was common::listing) — bounded recursive dir listing
- bundle + manifest (were minimal::diag::{bundle,manifest}) — the
tar+zstd BundleWriter and its self-describing manifest types
The host-specific collectors (which need a live daemon client, resolved
paths, and the machine's process/network state) stay in minimal::diag
and minimald::diag, now importing from `diagnostics`. BundleWriter no
longer bakes in the CLI version via env!("LONG_VERSION") — the crate is
app-agnostic, so create() takes it as a parameter.
Also hardens minimald/build.rs: git being unavailable (source tarball,
or a worktree whose .git isn't mounted in a cross container) degraded
to an unwrap panic; it now falls back to an empty hash.
Refs: #784
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maintainer review of the boot-log quiesce fix flagged three linked
smells: a hand-rolled `Arc<RwLock<Option<NonBlocking>>>` deferred
writer ("Wat"), a process-global `static Mutex<Option<Box<dyn FnOnce>>>`
release hook (fork-hazardous), and a free function mutating that global
state (doesn't scale).
Replace all three:
- the deferred writer becomes a `tracing_subscriber::reload` layer that
starts inert (records still reach the console) and is swapped to the
on-volume appender post-mount — the library's mechanism, no custom
MakeWriter. init_tracing returns a `LogActivator` closure so the
reload handle's generic type never surfaces in a signature.
- the release closure moves from the static into a `VolumeLogRelease`
field on `ServerState`, threaded async_main -> Server::run ->
ServerState::new (it can't ride in the serializable `Config`).
- the Shutdown RPC calls `s.release_volume_log().await` — a state
method, not a free function.
Behavior is unchanged: the on-volume appender's fd is still closed
before the quiesce, keeping the ext4 journal clean on a stop. Needs
the field re-validation (idle attach + `minvmd stop`) before merge.
Refs: #784
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reload refactor added a parameter to ServerStateHandle::new but the test-harness caller (gated behind the test-support feature, so the plain --all-targets clippy missed it) still called it with one arg, breaking the clippy/tests/root-integration CI lanes. Compile clippy with --all-features to catch feature-gated callers. Refs: #784 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
crates/minimald/src/server.rs (1)
236-244: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd coverage for one-shot volume-log release. Add a test that calls
release_volume_logtwice and asserts the closure runs only once, so the volume-backed fd is released before unmount.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/minimald/src/server.rs` around lines 236 - 244, Add coverage for Server::release_volume_log by configuring a volume-log release closure that records invocation, calling the method twice, and asserting the closure ran exactly once. Ensure the test verifies the one-shot behavior while preserving the existing microVM volume-unmount release flow.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/diagnostics/src/listing.rs`:
- Around line 41-51: Update the kind-character computation in the listing flow
to use the non-following result from entry.file_type(), so symlinks are reported
as l while preserving metadata() for size, mtime, and mode. Keep the existing
file_kind behavior for non-symlink entries and leave the recursion check
unchanged.
In `@crates/minimald/build.rs`:
- Around line 3-12: Update the git metadata and cleanliness-check flow in
build.rs to preserve whether Git status was unavailable instead of converting it
to a false dirty result. Only emit the dirty-check warning when the git status
command completes successfully and reports a dirty checkout; source archives or
environments without Git must retain an unknown state and produce no dirty-tree
warning.
---
Nitpick comments:
In `@crates/minimald/src/server.rs`:
- Around line 236-244: Add coverage for Server::release_volume_log by
configuring a volume-log release closure that records invocation, calling the
method twice, and asserting the closure ran exactly once. Ensure the test
verifies the one-shot behavior while preserving the existing microVM
volume-unmount release flow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dba2a88e-d6ba-4ae0-9e51-224f60fbfe93
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (22)
Cargo.tomlcrates/diagnostics/Cargo.tomlcrates/diagnostics/src/bundle.rscrates/diagnostics/src/lib.rscrates/diagnostics/src/listing.rscrates/diagnostics/src/manifest.rscrates/diagnostics/src/redact.rscrates/minimal/Cargo.tomlcrates/minimal/src/diag/collect.rscrates/minimal/src/diag/guest.rscrates/minimal/src/diag/mod.rscrates/minimal/src/diag/net.rscrates/minimal/src/diag/power.rscrates/minimal/src/diag/procs.rscrates/minimal/src/diag/redact.rscrates/minimald/Cargo.tomlcrates/minimald/build.rscrates/minimald/src/diag.rscrates/minimald/src/main.rscrates/minimald/src/rpc.rscrates/minimald/src/server.rscrates/minimald/src/test_harness.rs
🚧 Files skipped from review as they are similar to previous changes (8)
- crates/minimal/src/diag/redact.rs
- crates/minimald/src/rpc.rs
- crates/minimal/src/diag/mod.rs
- crates/minimal/Cargo.toml
- crates/minimal/src/diag/power.rs
- crates/minimald/src/diag.rs
- crates/minimal/src/diag/guest.rs
- crates/minimal/src/diag/net.rs
| let (kind, size, mtime, mode) = match entry.metadata() { | ||
| Ok(m) => ( | ||
| file_kind(&m), | ||
| m.len().to_string(), | ||
| m.mtime().to_string(), | ||
| format!("{:o}", m.mode() & 0o7777), | ||
| ), | ||
| Err(_) => ('?', "-".into(), "-".into(), "-".into()), | ||
| }; | ||
| let _ = writeln!(out, "{kind}\t{size}\t{mtime}\t{mode}\t{}", rel.display()); | ||
| if entry.file_type().is_ok_and(|t| t.is_dir()) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Symlinks report their target's type instead of l.
entry.metadata() follows symlinks, so file_kind returns the target's type (e.g., f or d) rather than l. Meanwhile, line 51 correctly uses entry.file_type() (no follow) for the recursion decision — so a symlinked directory is listed as d but never descended into, which is misleading. Use entry.file_type() for the kind character to stay consistent with the recursion check.
🔧 Proposed fix
fn walk_listing(root: &Path, dir: &Path, out: &mut String, count: &mut usize, max: usize) {
use std::fmt::Write as _;
use std::os::unix::fs::MetadataExt as _;
let Ok(entries) = std::fs::read_dir(dir) else {
let rel = dir.strip_prefix(root).unwrap_or(dir).display();
let _ = writeln!(out, "?\t-\t-\t-\t{rel}/ <unreadable>");
return;
};
let mut entries: Vec<_> = entries.flatten().collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
if *count >= max {
return;
}
*count += 1;
let path = entry.path();
let rel = path.strip_prefix(root).unwrap_or(&path);
- let (kind, size, mtime, mode) = match entry.metadata() {
- Ok(m) => (
- file_kind(&m),
- m.len().to_string(),
- m.mtime().to_string(),
- format!("{:o}", m.mode() & 0o7777),
- ),
- Err(_) => ('?', "-".into(), "-".into(), "-".into()),
- };
+ let (size, mtime, mode) = match entry.metadata() {
+ Ok(m) => (
+ m.len().to_string(),
+ m.mtime().to_string(),
+ format!("{:o}", m.mode() & 0o7777),
+ ),
+ Err(_) => ("-".into(), "-".into(), "-".into()),
+ };
+ let kind = match entry.file_type() {
+ Ok(ft) => file_kind(&ft),
+ Err(_) => '?',
+ };
let _ = writeln!(out, "{kind}\t{size}\t{mtime}\t{mode}\t{}", rel.display());
if entry.file_type().is_ok_and(|t| t.is_dir()) {
walk_listing(root, &path, out, count, max);
}
}
}
-fn file_kind(meta: &std::fs::Metadata) -> char {
- let ft = meta.file_type();
+fn file_kind(ft: &std::fs::FileType) -> char {
if ft.is_dir() {
'd'
} else if ft.is_symlink() {
'l'
} else if ft.is_file() {
'f'
} else {
's' // socket/fifo/device — "special"
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let (kind, size, mtime, mode) = match entry.metadata() { | |
| Ok(m) => ( | |
| file_kind(&m), | |
| m.len().to_string(), | |
| m.mtime().to_string(), | |
| format!("{:o}", m.mode() & 0o7777), | |
| ), | |
| Err(_) => ('?', "-".into(), "-".into(), "-".into()), | |
| }; | |
| let _ = writeln!(out, "{kind}\t{size}\t{mtime}\t{mode}\t{}", rel.display()); | |
| if entry.file_type().is_ok_and(|t| t.is_dir()) { | |
| let (size, mtime, mode) = match entry.metadata() { | |
| Ok(m) => ( | |
| m.len().to_string(), | |
| m.mtime().to_string(), | |
| format!("{:o}", m.mode() & 0o7777), | |
| ), | |
| Err(_) => ("-".into(), "-".into(), "-".into()), | |
| }; | |
| let kind = match entry.file_type() { | |
| Ok(ft) => file_kind(&ft), | |
| Err(_) => '?', | |
| }; | |
| let _ = writeln!(out, "{kind}\t{size}\t{mtime}\t{mode}\t{}", rel.display()); | |
| if entry.file_type().is_ok_and(|t| t.is_dir()) { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/diagnostics/src/listing.rs` around lines 41 - 51, Update the
kind-character computation in the listing flow to use the non-following result
from entry.file_type(), so symlinks are reported as l while preserving
metadata() for size, mtime, and mode. Keep the existing file_kind behavior for
non-symlink entries and leave the recursion check unchanged.
| // git may be unavailable (source tarball, a worktree whose .git is not | ||
| // mounted inside a cross container): degrade to an empty hash rather than | ||
| // panicking the build. | ||
| let git_hash = Command::new("git") | ||
| .args(["rev-parse", "--short", "HEAD"]) | ||
| .output() | ||
| .unwrap(); | ||
| let git_hash = String::from_utf8(output.stdout).unwrap(); | ||
| println!("cargo:rustc-env=GIT_HASH={}", git_hash); | ||
| .ok() | ||
| .and_then(|o| String::from_utf8(o.stdout).ok()) | ||
| .map(|s| s.trim().to_string()) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not report missing Git as a dirty checkout.
When git is unavailable, git_hash becomes empty, but the cleanliness probe fails to false, causing release builds from source archives to emit an inaccurate dirty-tree warning. Preserve an “unknown/unavailable” state and warn only after a successful git status.
Also applies to: 25-29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/minimald/build.rs` around lines 3 - 12, Update the git metadata and
cleanliness-check flow in build.rs to preserve whether Git status was
unavailable instead of converting it to a false dirty result. Only emit the
dirty-check warning when the git status command completes successfully and
reports a dirty checkout; source archives or environments without Git must
retain an unknown state and produce no dirty-tree warning.
|
This one has gotten out of hand - moving to draft |
* docs: add diagnostics subsystem spec and architecture Decomposes the min bug diagnostic bundle work (PR #784, moved to draft at +4107 lines) into eight demoable units, each an independently mergeable PR under 1000 lines. Records the design decisions from the review and research pass: app-agnostic machinery in a dedicated diagnostics crate, one manifest schema for both bundle layers, size-capped rotation via logroller, OTEL-compatible log conventions with zero OTEL dependencies, and a one-shot blob transport chosen for the wedged-process case. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add system and interface diagrams to the diagnostics architecture Four mermaid views: crate-boundary system overview with the degraded debugfs path, the full min bug collection sequence, the in-VM log pipeline with the release-before-quiesce ordering, and TRACEPARENT propagation across the CLI-daemon boundary. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: address CodeRabbit review on the diagnostics spec Pins the mode-specific manifest paths, upgrades the symlink guard to a no-follow open (TOCTOU-safe), gives the listing API failure/truncation semantics, adds the capture-timeout proof, layers the sensitive-key policy over the env allowlist, sets the per-prefix log-file cap at 5, corrects the Unit 3 artifact locations, standardizes the TRACEPARENT env name, bounds nested-bundle verification against decompression bombs, defines the argv key=value scrub and honest log/argv trust wording, exports the capture API in the curated surface, shows the serde defaults on DiagBundleRequest, and reconciles the traceparent fallback with R4.4. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: capture the guest-side incident trio in the daemon bundle New R6.6: the in-VM bundle gains routes+addresses (route + fib_trie), hang triage for the marker-matched family (pid-1, session hosts, task children), the fd-to-socket-inode join as the binary-free lsof equivalent, and allowlisted env. The partial wedge — daemon responsive while one binding or child is stuck, the #788 shape — was invisible to the guest bundle. R5.2 now requires the Linux mechanics be pure /proc reads so pid-1 can run them: the microVM rootfs has no lsof, ss, or ip. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: recast the diagnostics spec in functional language Removes issue/PR numbers and reviewer attributions from the spec and architecture doc. The motivating incident is described as a failure class, review guidance as settled design boundaries, and the prior branch strictly as the reference implementation baselines cite. The tracking-issue frontmatter is unchanged (house convention). Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: distinguish single-shot streaming from live streaming in the spec The bundle transport is itself a streaming RPC — one request, one streamed tar.zst, close — mirroring the wire's existing WorkspaceFilesTarZst subsystem pattern in the reverse direction. The spec's earlier 'not streaming' framing conflated that single-shot form with what it actually rejects: a long-lived stream of live debug data from a running daemon. R6.1 now cites the established pattern, and the Non-Goals, Design Considerations, and Alternatives sections use the precise terms. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: adopt the two zero-runtime OTEL-orbit crates in the spec Sharpens the Unit 4 boundary from 'zero OTEL deps' to 'zero OTEL runtime dependencies' and adopts the two crates that carry none of the rejected costs: opentelemetry-semantic-conventions (consts only) pins the resource-attribute names, and json-subscriber replaces the built-in JSON formatter — flat records with static top-level resource fields the stock formatter cannot emit, dependency closure already in the workspace, composing over the Unit 3 MakeWriter stack unchanged. Resource identity moves from a root span to top-level statics, which map onto the OTLP Resource exactly. Also corrects the future-seam pricing: prost is already resident, so an opentelemetry-proto OTLP-JSON emitter is cheaper than the general estimate. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record the log-pipeline backpressure coupling and the bridge boundary Technical Considerations now names the lossy(false) backpressure chain — a wedged volume can propagate through the bounded non_blocking channel into every logging daemon thread — as a deliberate coupling with its two mitigations (console layer independent of the volume; volume fallback collects without the daemon), backed by a new needs-spike ledger row measuring the headroom claim during Unit 3. The adopted-crates rationale states json-subscriber's execution honestly: log-write path of a healthy process only, no pipeline machinery, nothing at collection time. tracing-opentelemetry is named as the future export-seam crate and why it cannot be adopted piecemeal (id generation requires TracerProvider machinery; batch-buffered spans lose the tail on a hang). Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): land reusable collector mechanics in-crate from first use The host-bundle unit's generic mechanics (TOML redaction walker, masked process env, rotated-log selection, disk usage, first-line capture) move into the diagnostics crate at introduction rather than migrating later: each has a second consumer in the daemon bundle. The CLI contributes policy only — allowlist, prefixes, paths, archive layout. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): reconcile unit 3 baseline with shared versioning; mtime log ordering The per-crate build.rs the daemon-logs unit patched no longer exists — the shared version crate's build script already falls back cleanly without git, so the no-git-panic requirement is satisfied on the baseline and the unit carries no code for it. Rotated-log selection is documented as modified-time ordered: logrotate-style numeric shifting sorts opposite to date suffixes, so filename order cannot be the contract. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): system probe joins the diagnostics crate surface Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): reconcile the spec with the landed crate boundary The host-bundle unit's crate additions grew: the system probe joins the surface (disk paths as caller policy, best-effort fields), so the incident-collectors unit no longer migrates generic helpers — its requirement now asserts the crate rule holds rather than re-moving already-moved code. Collector requirements pick up the hardened contracts: filesystem errors are never reported as absence, every content read goes through the shared no-follow open, the listing walk runs on a blocking thread so collector timeouts stay effective, and rotation records the logrotate-style shift plus graceful archive publication that keeps an unmounting volume free of partial files. The system probe drops the version field claim — the manifest records the producer version once. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): drop the superseded unit-5 collect.rs sketch Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): revert rotation decision to tracing-appender daily logroller's size-based rotation is reverted for a smaller dependency surface and simpler shutdown; tracing-appender's daily appender rotates and prunes inline, so the volume-log release is a plain guard drop with no background thread to join and no partial intermediates on unmount. R3.6 becomes time-based with max_log_files retention; the design consideration, technical consideration, assumption ledger, and system diagrams follow, and the accepted tradeoff (no intra-day size cap) is recorded. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(specs): add a porting baseline for the remaining units The reference tree is a single 4107-line branch, but the merged units have reshaped the crate and daemon layout the later units build on. Add a Porting baseline section that maps the current state a fresh executor must reconcile against — the diagnostics crate's already-exported mechanics, the crates/minimal/src/diag layout, the minimald DaemonLogger and rpc dispatch anchor, and the minimald-rpc trace module — so Units 5-8 can be picked up without reverse-engineering the merged PRs. Re-anchor the drifted origin/main citation for the streaming-subsystem pattern on its grep-able constant rather than a line number. Refs: #801 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Adds a
min bugcommand that assembles a redacted diagnostic bundle (minimal-diag-<ts>.tar.zst) for support triage, plus the daemon-side streaming subsystem that captures in-VM state, the host-side captures needed to root-cause the field incidents in #788, and the crate/observability cleanups from review.Diagnostic bundle
min bugCLI (minimal): collects host system info, environment, config (redacted), state and workspace listings, logs, process tree, network state (listening sockets, interfaces with MACs masked to their OUI, routes), and per-provider daemon bundles — each collector failure-isolated into a manifest that explains every collected/skipped/errored entry.DiagBundleTarZstsubsystem (minimald-rpc,minimald): streaming RPC contract and daemon handler serving the in-VM daemon's own view, nested raw into the host bundle per provider; the client fails fast when a daemon refuses the subsystem.diagnostics): shared fail-closed key-based masking (JSON + TOML) and metadata-only listing, applied identically by the CLI and the daemon.#788 field-incident captures
boot.log— minvmd persists the VMM's hvc0 console to<provider>/boot.logby default (was dev-only), bundled per provider./logsfrom the ext4 image viadebugfs, with an offline-extraction hint otherwise.samples (macOS) / wchan+kernel-stack+fds (Linux) pluslsoffor the minimal process family, capped at 8 pids.pmset/journalctlsleep-wake transitions, so suspend/resume can be ruled in or out of a "stream died silently" report.Review cleanups
diagnosticscrate — the app-agnostic machinery (redact,listing,bundle,manifest) moved out ofcommon/minimal::diaginto a dedicated crate; host-specific collectors stay put and import it.tracing_subscriber::reloadlayer with the release closure owned byServerState(invoked via a method by the Shutdown RPC), replacing the hand-rolled deferred writer, the fork-hazardous process-global static mutex, and the free-function state change.vars_ospanic-proofing, subprocesskill_on_drop, 0600 bundle perms, symlink guards,--no-guesthonoring the probe, argv scoping on the daemon proc table, and more).Test plan
common,diagnostics,minimal,minimald(redaction, manifest, collectors, bundle streaming, socket probe) — verified via cross for the Linux-only crates-D warnings(incl.--all-featuresto cover feature-gated callers)minvmd stop, confirm the ext4 journal stays clean (the reload swap is compile/unit-verified; the microVM release-before-quiesce path is not exercised by the test suite)Related: #788 (field incidents this bundle diagnoses), #798 (keepalive fix, split out)
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
min bugcommand to create compressed diagnostic bundles with manifests.Bug Fixes