Skip to content

feat(minimal): guest fetch and degraded-mode fallback for min bug - #880

Merged
norrietaylor merged 3 commits into
mainfrom
feat/diag-unit7-guest-fetch
Jul 22, 2026
Merged

feat(minimal): guest fetch and degraded-mode fallback for min bug#880
norrietaylor merged 3 commits into
mainfrom
feat/diag-unit7-guest-fetch

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 21, 2026

Copy link
Copy Markdown
Member

Unit 7 of the diagnostics series (spec: #802, epic: #801).

Important

Stacked on #878 (Unit 6) and must merge after it. The base of this PR is feat/diag-unit6-daemon-bundle, not main — Unit 7 consumes Unit 6's DIAG_BUNDLE_SUBSYSTEM / DiagBundleRequest and the manifest-bearing guest bundle. Review the diff against that base; if GitHub shows Unit 6's changes too, Unit 6 has not merged yet.

What

min bug now reaches into every provider: a staged socket probe that says exactly where daemon contact breaks, the bundle download over the probe's own connection, and — when the daemon or transport is the suspect — vital signs and logs harvested read-only from the volume image. All of it is crates/minimal only; minimald/minimald-rpc are untouched.

client.rs (R7.2)Client::download_diag_bundle: writes one DiagBundleRequest, half-closes, and accumulates the streamed tar.zst up to a 256 MiB cap, returning (bytes, truncated). Daemon errors ride extended-data stream 1 (capped at 64 KiB) and become the Err, discarding any partial bundle; a bare subsystem refusal (Failure) fails fast rather than blocking until the caller's deadline. Sends the traceparent so the download joins the CLI's trace.

diag/net.rs (R7.1) — the SocketProbe: statconnect → SSH handshakeGetVersion, each a prerequisite for the next, recorded as providers/<name>/socket-probe.json. The failing stage is the diagnosis (no socket file / stale socket / wedged-behind-the-bridge / up-but-unhealthy). On a completed handshake it hands the live Client back so the download reuses the connection — no second handshake.

diag/guest.rscollect downloads and nests the daemon bundle raw at providers/<name>/guest/daemon-diag.tar.zst (R7.3); volume_fallback is the degraded path (R7.5); record_skipped writes the per-provider skip note (R7.4).

diag/collect.rs (R7.6)provider_files: per-provider dir-listing.txt, verbatim minvmd.toml, and status.json (raw lifecycle + non-mutating minvmd/minimald liveness via StateDir, deliberately not effective_state() which repairs stale state by writing it back).

diag/mod.rs — the real provider loop replaces the Unit 2 skip, plus --no-guest and --guest-timeout-secs (default 60).

R7.3 bounded decode (the one that has to be right)

The nested daemon bundle is stored raw either way; verification only decides whether the manifest records it as trustworthy. verify_nested_bundle drains the decompressed stream into a sink — bytes are counted and dropped, never materialized — and returns the instant a cap trips, so it never skips past a bomb's body. Three caps, each independent:

  • absolute decompressed ceiling — 1 GiB (the real memory/CPU guard);
  • entry count — 10,000 (the header-dimension bomb: millions of empty members, which the byte budget deliberately does not charge for);
  • manifest.json required (a well-formed daemon bundle always ends with one; absence means the stream is not what it claims).

A breach or decode failure still add_bytes-es the raw blob, then records a guest.<name>.verify manifest error naming the failed check (size check: / entry-count check: / manifest check: / decode check:).

Warning

Deviation from R7.3's expansion ratio. The spec writes "decompressed size capped at 4× the compressed size with a 1 GiB hard ceiling." A real diagnostic bundle is almost entirely compressible text (proc tables, JSON, logs); a representative host bundle measured 15.4× (586 KB content / 38 KB compressed), and the daemon bundle is the same shape. A literal 4× budget would flag every legitimate nested bundle as a bomb — the integration test asserts a healthy fetch verifies with zero guest.* errors, which a strict 4× would break. The implementation keeps the ratio but clamps the budget to [64 MiB, 1 GiB], so the two hard caps the bomb guard actually rests on (the 1 GiB ceiling and the 10k-entry cap) are exactly as specified, while the ratio no longer false-positives on real bundles. The NESTED_MIN_BUDGET floor and its rationale are documented at the constant. Flagging rather than silently shipping — happy to swap the floor value or revisit if the ratio was meant literally.

Porting deltas against the reference (77fc711e)

  • The reference stored the nested bundle with no verification at all; R7.3's bounded streaming decode is new in this unit, written to the spec and reconciled to the Unit 6 manifest (the reference predates the errors.jsonmanifest.json convergence, so its checks would have keyed on errors.json).
  • The reference's net.rs mixed the host network collectors (listening tables, interfaces, routes) with the probe. Those collectors merged into diagnostics::net in Unit 5, so only the SocketProbe half lands here; mask_macs and the interface/route collectors are not re-added.
  • provider_files does not re-collect run.log/boot.log — the merged tree's collect::logs already tail-caps them for every discovered provider, so a second copy would be a duplicate archive entry. (The reference collected them in both places, harmless only because its logs collector did not.)
  • DiagBundleRequest is constructed via ::default() (it is #[non_exhaustive]; a cross-crate struct literal will not compile).
  • tempfile promotes dev-dep → main dep (volume-harvest staging); tokio-stream is added for iterating the nested archive during verification; mod common; (the daemon harness) enters tests/bug.rs — all three per the spec's baseline notes.

Proof Artifacts

# Artifact Status
1 Test: full-stack — min bug against the harness daemon nests a guest bundle whose manifest parses; loadout redaction and client-key skip hold across layers — R7.1–R7.3 bug_with_daemon_nests_a_verified_guest_bundle
2 Test: stale socket file → probe records the failed connect stage and the skip is explained; volume-fallback artifacts appear — R7.1, R7.5 bug_with_stale_socket_reports_the_connect_stage_and_falls_back
3 Test: --no-guest against a live harness daemon performs zero daemon contact — R7.4 bug_no_guest_makes_no_daemon_contact
4 CLI (gate): kill a real VM's daemon mid-session, run min bug: probe stages + volume-meta.json + harvested volume-logs/ present — R7.5 end to end ⚠️ Not verified. Requires a real running microVM whose daemon is killed mid-session — an agent cannot honestly produce this. The volume-fallback path is exercised in-process by artifact 2 (probe fails, volume-meta.json written, debugfs path taken), but the live end-to-end gate against a real ext4 image has not been run.

The R7.3 caps have dedicated unit tests in diag::guest::tests: a_well_formed_bundle_verifies, a_bundle_without_a_manifest_fails_the_manifest_check, a_decompression_bomb_fails_the_size_check (a 72 MiB zero run that zstd squeezes below the budget floor), too_many_entries_fails_the_entry_check (10,001 members), and a_truncated_stream_fails_the_decode_check.

Verification

crates/minimal's test suite is Linux-onlyminimald is a dev-dependency and pulls procfs, so cargo test -p minimal does not build on the macOS dev host (pre-existing, not introduced here). It was verified via cross, not natively:

Command Result
cargo build -p minimal (native macOS) clean
cross clippy -p minimal --all-targets --target … -- -D warnings clean
cross test -p minimal --target … — lib 73 passed, 0 failed
cross test -p minimal --target …tests/bug.rs 5 passed, 2 failed — environmental, see below

Both cross invocations ran with CROSS_CONTAINER_OPTS="--env HOME=/tmp/xhome"; without a writable HOME some tests fail PermissionDenied on /.cache. CARGO_BUILD_JOBS=1 is also needed on this host — linking bin "min" and test "cli" concurrently OOM-kills ld inside Docker's 7.7 GiB VM (collect2: fatal error: ld terminated with signal 9), which looks like a compile failure but is not one. cargo clippy -p minimal --lib is not usable natively — it trips pre-existing macOS-cfg dead-code errors in crates/sandbox2 (needs_lib_symlink, network_namespaces_available, …) unrelated to this change — hence the cross clippy run for the real signal.

The 2 tests/bug.rs failures are pre-existing, not from this PR

incident_collectors_land_and_mask_macs and logs_collects_newest_five_per_prefix_and_provider_logs are Unit 5 tests (#864). This branch does not modify either — the only deletions in tests/bug.rs are four doc-comment lines. They fail because the cross musl image ships none of ip, ifconfig, ss, netstat, lsof (verified by inspecting the image directly), so host/net/interfaces.txt cannot be produced and the manifest carries 2 collector errors against an assert_eq!(errors, 0). CI's Linux lane is the authoritative environment for these two; they are called out here rather than left for a reviewer to trip over.

Two lint failures that were ours — fixed in f6c8a54a

The first --all-targets -D warnings run over this series caught both:

  • manual_clamp on the nested-bundle budget — the .max().min() pair is a clamp. The floor is a compile-time constant below the ceiling, so the rewrite cannot introduce the panic clippy warns about.
  • dead_code on common::TestDaemon::server — adding mod common; to tests/bug.rs gives that binary its own copy of the module, and it does not read the field tests/cli.rs does. expect would go unfulfilled in the cli binary, so allow is the accurate annotation.

Refs: #801

🤖 Generated with Claude Code

Note

Add guest bundle fetch and degraded-mode fallback to min bug diagnostic command

  • Adds Client::download_diag_bundle in client.rs to stream a tar+zstd diagnostic bundle from the daemon over SSH, returning collected bytes and a truncated flag with bounded error capture.
  • Adds per-provider guest collection in guest.rs: downloads and verifies the nested bundle, falls back to harvesting /logs from the ext4 volume via debugfs when the daemon is unreachable.
  • Adds socket reachability probing in net.rs, recording SSH handshake stage and persisting results as socket-probe.json in the bundle.
  • Extends collect.rs with provider files (dir listing, lifecycle state, liveness) and deferred, context-aware log-skip reasoning via explain_absent_log_prefixes.
  • Exposes --no-guest and --guest-timeout-secs (default 60s) CLI flags; tempfile and tokio-stream are promoted from dev-only to runtime dependencies.

Macroscope summarized 10c7830.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29153693-89fc-4eae-a51d-05dd57aa421f

📥 Commits

Reviewing files that changed from the base of the PR and between 356a08f and 10c7830.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • crates/minimal/Cargo.toml
  • crates/minimal/src/client.rs
  • crates/minimal/src/diag/collect.rs
  • crates/minimal/src/diag/guest.rs
  • crates/minimal/src/diag/mod.rs
  • crates/minimal/src/diag/net.rs
  • crates/minimal/tests/bug.rs
  • crates/minimal/tests/common/mod.rs
  • crates/minvmd/src/state.rs

Comment @coderabbitai help to get the list of available commands.

Comment thread crates/minimal/src/diag/guest.rs Outdated
Comment thread crates/minimal/src/diag/net.rs
Comment thread crates/minimal/src/diag/guest.rs
Comment thread crates/minimal/src/diag/collect.rs Outdated
Comment thread crates/minimal/src/diag/guest.rs Outdated
Comment thread crates/minimal/src/diag/collect.rs
Comment thread crates/minimal/src/diag/guest.rs
Comment thread crates/minimal/src/diag/guest.rs Outdated
Comment thread crates/minimal/src/diag/mod.rs Outdated
@norrietaylor

Copy link
Copy Markdown
Member Author

Two fixes pushed to feat/diag-unit7-guest-fetch1d696bca and 170bfb27. Both are additive to Unit 7; nothing else is restructured and the PR stays draft.


1. The manifest asserted the daemon logs were absent while carrying them

collect::logs skipped logs/minimald.log* with "no files with this prefix" the moment the host state dir held none. On macOS it never holds any — minimald runs inside the microVM and logs to the guest data volume. From a bundle captured during #869:

SKIPPED: logs/minimald.log* -> no files with this prefix

…with 178 KB of daemon logs sitting on the volume the whole time. Once Unit 7 lands, the same manifest.json carries that claim next to providers/<n>/guest/daemon-diag.tar.zst, whose own logs/ holds minimald.log.<date> — the files the claim says do not exist. The manifest's stated contract is that an absent file is always explainable, so a reader who trusts it stops looking. That is exactly what happened.

This is a regression, not a longstanding gap: the reference implementation (#784) emitted two skips, neither about logs. Unit 2 (#816) introduced this one on a temporal rationale — "the files only begin to exist after Unit 3" — which never considered that on macOS the host will never have them.

What changed

The claim was never wrong about the host; it was wrong about the bundle. Whether it is safe to make is not knowable when the log collector runs — collect_step!(w, "logs", …) fires before the provider loop, so "did a guest bundle arrive?" has not been answered yet. So:

  • collect::logs reports prefixes that matched nothing back through an out-param instead of skipping on the spot (the shared collect_step! contract pins its return type at Result<(), _>, so a return value was not available);
  • guest::collect now returns a GuestOutcome (BundleNested / NoBundle), which collect_guest propagates — this is the "pass it down" you asked for, just flowing the other way, since the log collector is upstream of the provider loop, not downstream;
  • after the loop, collect::explain_absent_log_prefixes records the held-back skips against a DaemonLogSources enum (NoProviders / NoneFetched / Nested(&[name])), so the illegal combination "no providers, yet a nested bundle" does not typecheck.

Plain absence is now claimed only where it holds of the bundle: for a host-only writer (minvmd.log*) or when no provider instance exists at all. Where the logs did arrive, the skip names the carrier. Real output from a min bug run on this machine (throwaway state dir, one provider, no live daemon):

logs/minvmd.log*   -> no files with this prefix in <state>/logs
logs/minimald.log* -> none in <state>/logs — where minimald runs inside the microVM it logs
                      to the guest data volume, not to this host, and no daemon bundle was
                      fetched this run; see providers/*/guest/ for why

and with a bundle nested, the same skip ends … Its own logs are in this bundle, nested in providers/local-0/guest/daemon-diag.tar.zst (see logs/PROVENANCE.txt).

The test is the actual fix

no_skip_claims_absence_for_a_path_the_bundle_carries asserts the invariant, not the wording: it walks every manifest skip whose reason claims absence and requires that no path the archive holds — at any nesting level, nested .tar.zst included — is named by it. The absence phrasing lives in one const NO_SUCH_FILES so the test can find every skip that makes the claim. Non-vacuous in both directions: it also asserts the claim is still made for logs/minvmd.log* (so "fix by never claiming absence" fails it) and that the minimald skip names its carrier. The pre-fix reason was NO_SUCH_FILES verbatim, so the nested logs/minimald.log.2026-07-16 in the test's synthesised guest bundle would trip the assert — though the test cannot literally be compiled against the old code, since the function it drives did not exist.

One level down, untouched: crates/minimald/src/diag.rs:357 emits the same phrasing for logs/minimald.log* inside the guest bundle. There the claim is true of that bundle, so I left it — but note the nested manifest can still say "no files with this prefix" while the outer bundle's providers/<n>/boot.log holds daemon records. That trap is what the provenance note below now spells out.


2. Nothing told a reader which log was which

After Unit 7 a bundle carries two overlapping copies of the daemon's output — providers/<n>/boot.log (hvc0 console mirror, File::create'd per boot) and, inside the guest bundle, the on-volume appender (14 daily files). They are not duplicates and neither is a superset:

  • console only — 4 records of boot prologue before the state volume mounts (mounted pseudo filesystem, switched to upstream rootfs, mounted writable state volume, cache + state relocated);
  • console only — 5 records at shutdown: the appender is released before quiesce, so state volume quiesced, draining connections and the final close exist nowhere else;
  • volume only — nothing in the incident window; the two were byte-identical modulo \r over the overlap;
  • and post-Unit-4 the volume log is JSON lines while the console stays human-format, so they are not diffable without normalisation.

logs/PROVENANCE.txt (2.2 KB, one entry in the bundle) states per log path: source, retention, format, and the known holes.

Why the file and not a source field on CollectedEntry

CollectedEntry is #[non_exhaustive], so a field is additive at the type level — but it is not free:

  • it changes the manifest shape both producers emit, which is a schema_version question and an amendment to R1.3, which pins the manifest's exact keys;
  • every collector in both crates would have to supply a value or it ships mostly-null;
  • and a scalar per entry cannot carry "what this source structurally cannot contain", which is the part a reader actually needs — "console mirror" does not tell you the volume log has no boot prologue.

A bundle entry costs no schema: schema_version stays 1, diag-explore needs no change, and the note appears in manifest.collected like anything else. If you'd rather have the field as well, it slots in later without conflicting with this.


Spec amendments I think are needed

Both fixes go beyond what spec 10 (#802, origin/arch/diagnostics-spec) currently says. Flagging rather than silently diverging — I did not edit the spec branch:

  1. R2.7 — "Absent files are manifest skips, not errors (the files only begin to exist after Unit 3 — best-effort by design)." The parenthetical is the rationale that produced the bug. Suggest replacing it with the durable reason: an absence claim is scoped to the directory actually searched, and where a daemon may run inside the microVM the skip must point at the guest artifacts rather than assert absence.
  2. R2.2 — "absence claims … may only be made on a true NotFound" is necessary but not sufficient; this skip was a true NotFound. Suggest extending: …and only when the bundle does not carry the named path by another route, nested bundles included.
  3. New requirement (R2.10 or an R2.7 clause) — the bundle shall carry a log-provenance note naming, per log path, the source, retention, and known holes. Nothing in the spec requires it today, and Unit 8's explorer may want to surface it.

Correction: the degraded-mode debugfs fallback

An earlier review called this fallback inert on macOS because which debugfs fails. The conclusion is right, the cause is not: e2fsprogs is installed, keg-only, at /opt/homebrew/opt/e2fsprogs/sbin/debugfs (debugfs 1.47.4). guest.rs resolves it by bare name (Command::new("debugfs"), in both volume_fallback and logs_tree_bytes), so PATH lookup misses the keg and R7.5 degrades to volume-meta.json plus the offline hint. Measured just now against a fake image:

providers/local-0/guest/volume-logs -> debugfs unavailable (No such file or directory (os error 2)); harvest offline with: …

That is ENOENT on exec, not on the image. I left it alone — out of scope for the two fixes you asked for — but it is a one-liner (probe the keg path when the bare name misses, or honour a MINIMAL_DEBUGFS override) and it is what stands between R7.5 and working on the primary dev platform.


Verification

Command Result
cargo build -p minimal (native macOS) clean
cargo clippy -p minimal --lib --no-deps -- -D warnings clean
cargo fmt -p minimal --check clean
min bug against a throwaway --minimal-dir (no live daemon) 21 entries, 0 errors; both skip reasons and logs/PROVENANCE.txt verified in the manifest
cargo clippy -p minimal --all-targets cannot run here — pre-existing: the minimald dev-dep pulls hakoniwa → libcgroups → procfs, whose build script hard-fails off-Linux (verified again on this branch)
cargo test -p minimalthe two new tests ⚠️ not executed. Same procfs blocker; a cross test -p minimal --lib --target aarch64-unknown-linux-musl run (the recipe in the PR body, HOME=/tmp/xhome, CARGO_BUILD_JOBS=1) was still compiling dependencies after ~25 min against a cold musl target dir and was abandoned, not failed. Please treat both new tests as unrun until the Linux lane reports.

So: the code paths are verified natively through the real binary, and the test assertions over them are not. The one branch neither covers is DaemonLogSources::Nested end to end — the unit test drives it with a synthesised daemon bundle rather than a real fetch. Explicitly not run: anything needing a live microVM; artifact 4 in the PR body (kill a real VM's daemon mid-session) is still unverified and this does not change that.

Likely conflict ahead: another change in flight adds a --log-tail-bytes flag off main, touching crates/minimal/src/diag/. It will almost certainly want to plumb a cap through collect::logs, whose signature I just changed (added the out-param) and whose body I edited around LOG_TAIL_CAP. Same function, same lines — whoever merges second should expect to resolve it by hand.

@norrietaylor
norrietaylor marked this pull request as ready for review July 22, 2026 04:54
@norrietaylor

Copy link
Copy Markdown
Member Author

@macroscope review

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Manual reviews triggered for commit 170bfb2:

All prior checks · these links stay valid even if you push more commits.

@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Just FYI for future @mentions, I'm Macroscope-App, not Macroscope.

Review triggered. Results will be posted as check runs when complete.

Comment thread crates/minimal/src/client.rs
@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces new guest bundle fetching and degraded-mode fallback capabilities with substantial new logic. Two unresolved review comments identify potential correctness issues in error handling that should be addressed before merging.

You can customize Macroscope's approvability policy. Learn more.

Comment thread crates/minimal/src/diag/guest.rs
Comment thread crates/minimal/src/diag/guest.rs
@norrietaylor

Copy link
Copy Markdown
Member Author

Review round worked — 2 open findings closed, 8 confirmed already closed, 1 adjacent defect fixed

Five commits on feat/diag-unit7-guest-fetch. Nothing rebased, nothing force-pushed, body untouched.

Commit What
93458897 fix(minimal): stop the log skips claiming what the run never established — diag/mod.rs:162 and its linked location collect.rs:364
70e0f200 fix(minimal): observe a daemon error queued when the bundle cap trips — client.rs:284
105522fe fix(minimal): resolve debugfs outside PATH so the volume fallback runs — not from this round, see below
a5a013d8 test(minimal): cover the PAX extension bypass the stream cap closes
72fe20ac docs(minimal): correct collect_guest's return description

Verdicts

Open at the start of this pass — both valid, both fixed.

diag/mod.rs:162 (Medium) — fixed. Reproduced first, against a binary built from 170bfb27, with chmod 000 providers on a throwaway state dir:

logs/minimald.log* -> no files with this prefix in <state>/logs
logs/minvmd.log*   -> no files with this prefix in <state>/logs

…in a manifest that also carried two Permission denied errors. The mechanism is as described: absent is filled by the prefix loop before collect::logs reaches its own provider_dirs(...)?, so the prefixes survive the collector's failure and reach a DaemonLogSources computed from a listing that never happened. providers is now Option<Vec<_>> and DaemonLogSources gained ProvidersUnknown. minvmd.log* deliberately still claims plain absence there — host-only writer, unaffected by a provider listing failure.

collect.rs:364 (linked location) — fixed in the same commit. BundleNested was returned for any stored bytes, so a truncated or verification-failed blob got named as the carrier of the daemon's logs. verify_nested_bundle now also reports whether it saw logs/minimald.log* go past — free, since it already walks every entry path for the manifest check — and GuestOutcome splits confirmed from unconfirmed. An unconfirmed bundle is still named, as somewhere to look, never as the answer.

client.rs:284 (Medium) — valid, fixed, but not as suggested. Draining to the end of the channel is unbounded: the daemon only reports a build failure after its tokio::io::copy returns, guest::collect caps this call at --guest-timeout-secs (60s), and the daemon's own stream deadline is 300s. So the suggested drain trades a usable 256 MiB truncated bundle for a timeout and nothing — and the message it would most often recover, client stopped reading the bundle, is one the drain provokes itself. Landed a bounded grace-window drain instead (payload discarded, extended data kept) and corrected the method's contract to state the residual rather than promise what the code does not do. Full reasoning in thread.

Already closed before this pass — verified present in the tree at 170bfb27, read at the call site rather than taken on the resolution marker. collect.rs:332 (High — open_regular_nofollow), guest.rs:138 (High — stream cap under the parser), guest.rs:210 (NotFound distinguished), net.rs:114 (connect stage re-tested), collect.rs:367 (StateDir::open_existing), guest.rs:238 (pre-flight ls -l sizing), plus the two Macroscope self-merged via #889 / #890.

One change that is not from this round

105522fe fixes debugfs resolution. Adjacent rather than requested: two of this round's findings landed inside volume_fallback / logs_tree_bytes, which are its only two callers, and I had previously measured this defect and left it alone as out of scope. Both sites spawned debugfs by bare name, so R7.5 was inert wherever e2fsprogs is not on PATH — keg-only under Homebrew, /sbin on Linux. Measured here against a real ext4 image built with mke2fs -d:

before:  volume-logs -> debugfs unavailable (No such file or directory (os error 2))
after:   volume-logs/minimald.log.2026-07-20
         volume-logs/minimald.log.2026-07-21          (24 entries, 0 errors)

ENOENT on exec, not on the image — on the platform where minimald runs inside the microVM and the volume is the only place its logs exist. Separated into its own commit so it can be dropped independently if it is not wanted here.

Pre-existing, deliberately not fixed

diagnostics::newest_rotated collapses a read_dir failure into an empty result, so a <state>/logs that stats but will not enumerate reaches absent as though it were empty — the same defect class as mod.rs:162, one layer down. It is shared with minimald, so changing its signature belongs with Unit 6.

Verification

Command Result
cargo build -p minimal (native macOS) clean
cargo clippy -p minimal --lib --no-deps -- -D warnings (native) clean
cross clippy -p minimal --all-targets … -- -D warnings clean
cross test -p minimal --lib … 79 passed; 0 failed
cross test -p minimal --test bug … 5 passed; 2 failed — the two documented environmental failures, unchanged

The two tests added in 1d696bca that had never been run are now run, and pass:

test diag::collect::tests::no_skip_claims_absence_for_a_path_the_bundle_carries ... ok
test diag::collect::tests::the_log_provenance_note_distinguishes_the_two_daemon_log_sources ... ok

alongside the four added here:

test diag::collect::tests::plain_absence_is_claimed_only_where_the_run_established_it ... ok
test diag::collect::tests::only_a_confirmed_nested_bundle_is_named_as_the_carrier ... ok
test diag::guest::tests::a_pax_extension_bomb_is_stopped_by_the_stream_cap ... ok
test diag::guest::tests::verification_reports_whether_the_daemon_logs_are_present ... ok

test result: ok. 79 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.88s

The three guest-fetch integration tests all pass (bug_with_daemon_nests_a_verified_guest_bundle, bug_with_stale_socket_reports_the_connect_stage_and_falls_back, bug_no_guest_makes_no_daemon_contact). The 2 failures are the Unit 5 tests this branch does not touch, and I re-confirmed the cause directly rather than inheriting the claim — inside the cross image:

ip        MISSING
ifconfig  MISSING
ss        MISSING
netstat   MISSING
lsof      MISSING

so host/net/interfaces.txt cannot be produced and 2 collector errors land against assert_eq!(errors, 0). Same 5/2 split as before this pass — no new failures.

cargo test -p minimal --all-targets still cannot build natively; re-confirmed, and the cause is one layer wider than "procfs": both procfs (Building procfs on an for a unsupported platform) and caps fail on macOS via the minimald dev-dep.

Merge conflict expected

#898 (--log-tail-bytes) touches collect::logs, whose signature this branch already changed in 1d696bca. Not pre-resolved — flagging it so whoever merges second knows it is expected.

Comment thread scripts/bulk-upload-e2e.sh
Base automatically changed from feat/diag-unit6-daemon-bundle to main July 22, 2026 17:53
@norrietaylor
norrietaylor enabled auto-merge (squash) July 22, 2026 18:01
norrietaylor and others added 2 commits July 22, 2026 11:18
Per provider, `min bug` now performs the staged socket probe and — when
the probe handshakes — downloads the daemon's own bundle over the
DiagBundleTarZst subsystem, nesting it under providers/<name>/guest/.
--no-guest skips daemon contact entirely; --guest-timeout-secs bounds
each provider's download. Host-side log-prefix skips are deferred until
the provider loop settles whether the daemon's logs reached the bundle
another way, so the manifest never claims an absence the archive does
not back.

Squashed rebuild of the original branch onto main after #878 landed
there, replacing the merge-heavy history whose #889 squash title also
failed commitlint.

Refs: #802

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #895 changed the serve! dispatch so every handler returns
Result<(), ConnectionError> for outcome logging; PR #878 merged
alongside it with serve_stream_diag_bundle still returning (). The two
were each green against a main that lacked the other, and the type
mismatch only surfaced on branches built after both landed.

Failure still relays the message over the channel's extended-data
stream before surfacing as ConnectionError::Internal, mirroring
serve_stream_workspace_files.

Refs: #878, #895

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@norrietaylor
norrietaylor enabled auto-merge (squash) July 22, 2026 18:43
@norrietaylor
norrietaylor merged commit 159f883 into main Jul 22, 2026
29 checks passed
@norrietaylor
norrietaylor deleted the feat/diag-unit7-guest-fetch branch July 22, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants