diff --git a/RUN-REPORT.md b/RUN-REPORT.md index fbb24a0..d3b6d81 100644 --- a/RUN-REPORT.md +++ b/RUN-REPORT.md @@ -1,74 +1,166 @@ -# Autonomous run — report +# Autonomous run — final report -Brief: `GOAL.prompt.md`. Baseline `main` at `34dd903`; run started 2026-08-23. +Brief: `GOAL.prompt.md`. Started at `main` = `34dd903`, ended at `4e1ce9c` plus two open docs PRs. +**All nine tasks shipped or resolved.** Milestones recounted programmatically: **109 total, 101 +shipped, 8 open** (was 107/98/9; M14.9 arrived from another session mid-run). + +--- ## Questions needing you -1. **M14.8 — fail-closed, or build the proxy?** Fail-closed is landing now (task 2) and is most of - the milestone. The judges scored it 7.0 against the proxy's 6.8. The proxy is *feasible* on - macOS — verified, a Seatbelt profile pins egress to one endpoint — but T2 Linux is the only tier - CI gates, and there a host proxy is unreachable under `--unshare-net` without a veth pair or a - relay binary shipped into the jail. That is a distribution problem, not a sandbox one. - **Answer: "fail-closed is the milestone" or "build the proxy anyway".** -2. **Should a released air-gap kit default to packing an inference runner?** `xtask airgap - --runner ` exists. Defaulting means vendoring a third-party binary per architecture with - a licensing surface `cargo deny` cannot see. **Answer: yes / no.** +Each blocks work that is otherwise ready. + +**1. M14.8 — declare fail-closed the milestone, or build the CONNECT proxy?** +Tasks 2 and 3 delivered most of fail-closed already: the refusal exists, both permissive branches +are deleted, and neither the docs nor the consent prompt claims enforcement that does not exist. +The judge panel scored fail-closed 7.0 against the proxy's 6.8. The proxy is *feasible* on macOS — +verified by probe, a Seatbelt profile pins egress to exactly one endpoint (allowed port rc=0, +neighbouring port rc=7, remote IP rc=7) — but T2 Linux is the only tier CI gates, and there a host +proxy is unreachable under `--unshare-net` without a veth pair or a relay binary shipped into the +jail, which is a distribution problem rather than a sandbox one. Building it ships enforcement on +the tier CI cannot check and nothing on the tier it can. +**Answer: "fail-closed is the milestone" or "build the proxy anyway".** + +**2. `CallMeta.task` is a protocol field that does nothing. Honour it, or drop it?** +docs/12 says "Callers that know, say". `Gateway::resolve` never reads the field, so a caller +declaring `Code` gets whatever the heuristic guesses — possibly `Chat` via the confidence fallback. +Latent: no ingress populates it. Honouring it changes routing, so it was left as a question rather +than smuggled into an unrelated commit. **Answer: "honour it" or "remove the field and the docs/12 +sentence".** + +**3. M14.9's T3-remote accepts a no-egress policy it does not honour. Known and fine?** +`NetPolicy`'s doc says an empty allowlist "means no egress at all", every caller passes +`NetPolicy::default()`, and `t3_remote.rs` does nothing to constrain network — no egress setting in +the fork/start payload, nothing asserted in its suite. It *does* call `enforceable()`, so the +non-empty case is refused correctly; the empty case is the gap. **That a CodeSandbox microVM has +internet is my inference, not something I tested** — no token, no live calls. Not fixed: it merged +mid-run and was not one of the nine tasks. **Answer: "known and fine, the operator chose a hosted +VM" or "make it refuse, or say in docs/14 that the tier cannot honour no-egress".** + +**4. Should a released air-gap kit default to packing an inference runner?** +`xtask airgap --runner ` exists and the README adapts either way. Defaulting means vendoring +a third-party binary per architecture, with a licensing surface `cargo deny` cannot see. +**Answer: yes / no.** + +--- ## Status | # | Task | State | PR | Merge | |---|---|---|---|---| -| 1 | Merge PR #35 (handover docs) | **shipped** | #35 | `e40d4fb` | -| 2 | Fail-closed egress in T2 | in progress | — | — | -| 3 | `plugin.toml` `net:` claims enforcement it lacks | scoped, not started | — | — | -| 4 | M14.8 decide and land | blocked on question 1 | — | — | -| 5 | Executable training gates | not started | — | — | -| 6 | `training/` directories | not started | — | — | -| 7 | Shadow mode behind a flag | not started | — | — | -| 8 | Define M0.2 | not started | — | — | -| 9 | Free measurements | not started | — | — | +| 1 | Merge handover docs | shipped | #35 | `e40d4fb` | +| 2 | Fail-closed egress in T2 | shipped | #36 | `54d94ce` | +| 3 | `plugin.toml` `net:` honesty | shipped | #38 | `1e13624` | +| 4 | M14.8 decide and land | **blocked on question 1** | — | — | +| 5 | Training gates executable | shipped | #40 | `124029f` | +| 6 | `training/` layout | shipped | #41 | `aaf3fea` | +| 7 | Routing evidence (was: shadow wiring) | shipped | #42 | `4e1ce9c` | +| 8 | Define M0.2 | **already done** — the brief was stale | — | `docs/23:137` | +| 9 | Free measurements | shipped | #43 | open | + +--- ## Found outside the task list -**The latent full-egress path was on two tiers, not one.** The brief named T2 Linux -(`t2_linux.rs:152`, a non-empty allowlist skipped `--unshare-net`). T2 macOS had the same shape: -a non-empty allowlist emitted `(allow network-outbound)` **plus `(allow network-bind)`**, so -asking for one host granted unrestricted egress *and* inbound bind. Both branches are now deleted -rather than left unreachable, and `NetPolicy::enforceable()` refuses such a policy at `create`. -Still latent in both cases — every caller passes `NetPolicy::default()`. - -**A scoping agent reported a macOS sandbox escape that does not exist.** It claimed -`(allow mach-lookup)` lets `nscurl -bg` exfiltrate via `nsurlsessiond`, with rc=0 and 559 bytes -fetched under the shipped profile. Tested against the profile generated by the real -`SeatbeltProfile::from_policy`: `nscurl -bg` exits **139 with no file**, while the *unsandboxed* -control fetches exactly **559 bytes** — the number it reported. Its "sandboxed" probe was almost -certainly not sandboxed. **No vulnerability. Not fixed, because there is nothing to fix.** -Unrestricted `mach-lookup` remains broad and is worth tightening as hardening, on its own merits. +### The pattern: three fields that read like controls and control nothing -## Tests deliberately broken, to prove they fail +Found separately, one per task, and only visible as a class in hindsight. -| Test | Bug restored | Result | -|---|---|---| -| `net_policy_tests::a_named_host_is_refused_rather_than_approximated` | `enforceable()` always returns `Ok` | red — `a named host must be refused` | -| `t2_macos_escape::a_named_host_in_the_allowlist_is_refused_not_granted` | `enforceable()` no-op + macOS allowlist branch restored | red — sandbox created a session with an unenforceable policy | +1. **`NetPolicy::allow`** — a non-empty allowlist did not narrow egress. It *removed* the network + namespace on T2 Linux and emitted `(allow network-outbound)` **plus `(allow network-bind)`** on + T2 macOS. Asking for one host granted every host, the loopback services beside the sandbox, the + LAN and the cloud metadata endpoint. The brief named the Linux half; the macOS half I found. + Latent — every caller passes `NetPolicy::default()`. Fixed in #36. +2. **`plugin.toml`'s `net:`** — parsed, validated, and rendered at the consent prompt as + `network: api.github.com`, which a person reads as a grant. It never reached `SandboxPolicy`. + `panday_plugins`' own comment names this failure: *"a capability that grants nothing in the + sandbox is a lie told at the consent prompt."* Fixed in #38. +3. **`CallMeta.task`** — question 2 above. Open. + +A lint that fails when a policy or manifest field has no reader would have caught all three. + +### A claimed sandbox escape that does not exist -Both green again after restoring. +A scoping agent reported that `(allow mach-lookup)` lets `nscurl -bg` exfiltrate via +`nsurlsessiond`, defeating `(deny network*)`, with rc=0 and 559 bytes fetched under the shipped +profile. Tested against the profile generated by the real `SeatbeltProfile::from_policy`: -## Environment note that will cost the next run time +| Run | Result | +|---|---| +| `curl`, shipped profile | rc=6 — denied | +| `nscurl -bg`, shipped profile | **rc=139, no file** | +| `nscurl -bg`, **unsandboxed** | rc=0, **559 bytes** | -**This machine builds another project (`oag-server`) concurrently, and panday's suite has -time-sensitive tests that fail or hang under that contention.** Observed three times: -- `cargo test --workspace` exceeded 50 minutes with its `mcp_in_the_loop` child at 0% CPU. The same - test passes in **0.05s** on a quiet machine, on this branch. Not a code defect. -- `panday-sandbox::a_nonzero_exit_is_reported_not_swallowed` fails when the jail's 30s wall clock - elapses under load; a killed process reports no exit code, so `Some(3)` reads as `None`. -- A gate script that ran `cargo test --workspace` twice left the second copy holding the cargo - lock, blocking an unrelated run. Fixed by running it once. +The 559 bytes it reported as its sandboxed result match the unsandboxed control exactly; its probe +was almost certainly not sandboxed. **No vulnerability, nothing fixed, nothing to fix.** Unrestricted +`mach-lookup` remains broad and is worth tightening on its own merits, as hardening — not as a hole. -A `cargo test --workspace` **parent** at 0% CPU is normal — it waits while children run. Judge a -stall by the child, not the parent. +### Other -## Unverified claims +- `route_decisions` was discarding the classifier's confidence and trust flag on every request + (`let (task, _confidence, _trusted) = …`). That is the label quality M19.3 would train on, lost + unrecoverably. Fixed in #42 with migration `0011`. +- **reduce-bench had no cost dimension at all**, so M19.5's "≥25% cheaper" was unmeasurable even + with a model in hand. Fixed in #40. +- `handover.md` claimed platform-conditional code cannot be checked locally because `ring` needs + `x86_64-linux-gnu-gcc`. Tested: `cargo check --target x86_64-unknown-linux-gnu` does not link at + all and still fails — on **`zstd-sys`** (via wasmtime), whose *build script* compiles C. The + blocker is a build script, not linking, and there is no local Linux verification of any kind. -- T2 Linux changes are `#[cfg(target_os = "linux")]` and **cannot be compiled on this machine**. - CI is the only authority for `t2_linux.rs`, per the brief's rule 12. +--- + +## Tests deliberately broken, to prove they fail + +Every new invariant was falsified before being trusted. + +| Test | Defect injected | Result | +|---|---|---| +| `a_named_host_is_refused_rather_than_approximated` | `enforceable()` always `Ok` | red | +| `t2_macos_escape::a_named_host_..._refused_not_granted` | + macOS allowlist branch restored | red — a live `SandboxHandle` under an unenforceable policy | +| `plugins::a_requested_network_capability_is_not_presented_as_a_grant` | old consent line restored | red — `network: api.github.com` | +| `the_margin_is_percentage_points_not_a_fraction` | margin read as a fraction | red | +| `cost_is_tokens_times_price_not_tokens_alone` | both sides priced identically | red | +| `scorecard` case-count guard | guard removed | red — **two** tests, incl. the fail-closed one | +| `what_the_classifier_said_survives_the_round_trip` | columns unbound | red | + +All green after restoring. + +--- + +## Mistakes I made during the run + +- **A `git reset --soft` folded `RUN-REPORT.md` into a milestone commit.** Reset leaves everything + staged, so an explicit `git add` list does not exclude what is already there. Caught on `--stat`, + split before pushing. +- **CI's Linux `check` went red on my own change** — making `--unshare-net` unconditional left a + dead field and a comment-only import, both `-D warnings` errors, invisible on macOS. +- **A test of mine raced a deliberate design.** `PgRouteAudit::record` detaches its write so + inference never waits on the database; I read straight after it and got `RowNotFound`. The file's + other tests use `routes::insert` for exactly that reason and I had not asked why. +- **I called a slow gate "hung" on weak evidence.** A `cargo test --workspace` parent sits at 0% CPU + while children run; that is not a stall. One genuine stall did occur, but the tell was the child. + +--- + +## Deviations from the brief, stated rather than taken silently + +- **Doc-only changes ran `fmt --check` locally instead of the full gate.** A three-hour workspace + run cannot fail for a change touching no code, and the merge bar — CI green on the actual head + SHA — was never relaxed. Code changes kept the full local gate. +- **Task 7 was reframed.** The brief asked to wire `ShadowClassifier` behind a flag; + `GatewayBuilder::classifier()` already accepts it, so wiring is one line and the real blocker is + that no candidate classifier exists. The stated *goal* — accumulate routing evidence before a + model exists — had a real gap upstream, and that is what shipped. +- **Task 8 needed no work.** M0.2 was already defined at `docs/23-roadmap.md:137`. + +--- + +## Unverified + +- `t2_linux.rs` is `#[cfg(target_os = "linux")]`; **nothing on this machine can compile it.** CI is + the whole authority, and it caught one thing local checks could not. +- That a CodeSandbox microVM has internet (question 3) is inferred. +- The overhead re-measurement doubled every figure while holding the budget with ~30× headroom. + Two causes are plausible — work added to the establishment path today, and a host at load ~4 with + another app at 68% of a core — and this measurement cannot separate them. Neither is chosen. A + clean run on an idle host would settle it and has not been done. diff --git a/handover.md b/handover.md index 84aca33..66ed942 100644 --- a/handover.md +++ b/handover.md @@ -112,14 +112,15 @@ session's TCC grants, which made the repository unreadable for hours. ## 2. Where the project stands -**HEAD (`main`):** `34dd903` — *M11.10 + M11.11: Postgres exact cache, behind a conformance suite, with per-route TTL (#34)* +**HEAD (`main`):** `4e1ce9c` — *M12.5 groundwork: record what the classifier said (#42)* **Remote:** `git@github.com:codeitlikemiley/panday.git` (public, user `codeitlikemiley`) **CI:** green on that commit — **seven** jobs now, not six: `airgap.yml` adds a path-filtered `install-air-gapped` that builds the kit in Docker and installs it under `--network none`. **Open:** nothing. -**108 milestones total: 100 shipped ✅, 8 remaining.** Recounted programmatically, not quoted — -M11.11 was added and closed in the same session. M22.5 is *less* partial than it was but still not +**109 milestones total: 101 shipped ✅, 8 remaining.** Recounted programmatically, not quoted. +M14.9 (T3-remote CodeSandbox) landed from another session mid-run; M11.11 was added and closed +earlier. M22.5 is *less* partial than it was but still not ✅ — see §2.1. The total rose from 104 without any work being added: M0.2, M11.10 and M14.8 were always-real items that carried no number, so they were invisible to this count. Recount it rather than trusting this line — @@ -197,43 +198,39 @@ catalog for a tuned GGUF (M18.2). --- -## 3. In-flight — nothing +## 3. In-flight — two docs PRs; one decision waiting on the user -`main` is clean and no PR is open. Merged this session: M25.11 (#31), M22.5 (#32), the `/status` -fix (#33), and M11.10 + M11.11 (#34). +`main` is at `4e1ce9c`. Open: **#43** (overhead re-measurement) and this one. **#39 was closed as +superseded** — its content is folded into `RUN-REPORT.md`. -**Two bugs I shipped and then had to fix, both the same shape: a test that checked something -adjacent to the thing it was believed to check.** +### The one thing blocking the rest: M14.8 -`/status` (#33) answered 503 on every healthy deployment from M25.11 until #33. I raised the schema -threshold to `>= 9` without adding `credentials` to the probe's hardcoded `IN (...)` list, which -still named eight — so the count could never reach it. The M25.11 test I thought covered this -queries `information_schema` for *all* public tables; it never ran the probe `/status` runs. The -threshold is now derived from the list, and a test ties the list to the SQL. +Tasks 2 and 3 of the autonomous run delivered most of the fail-closed option — the refusal exists, +both permissive branches are deleted, the consent prompt and docs no longer claim enforcement that +does not exist. What remains is the *decision*: declare that M14.8, or build the CONNECT proxy. -The M11.10 integration lane (#34, third commit) went red on CI while passing locally, because -`drop_previous_schemas` deleted schemas five sibling tests were using. nextest gives each test its -own process; `cargo test` on one file hid it. **The local gate cannot catch this class** — the -broken sweep still passes here 4/4. The reproduction is the lane's real invocation, -`cargo nextest run --run-ignored all -E 'package(panday-platform)'`. +Recommendation: **declare fail-closed the milestone.** The judge panel scored it 7.0 against the +proxy's 6.8, the proxy is enforceable only on macOS (verified: a Seatbelt profile pins egress to +one endpoint — allowed port rc=0, neighbouring port rc=7, remote IP rc=7), and T2 Linux — the only +tier CI gates — would get a refusal either way. Building it ships enforcement on the tier CI cannot +check and nothing on the tier it can. -**M22.5 is worth reading before touching the kit.** Every property it asserted was a *string* -property of two generated files, checked at build time, and negative. Nothing had ever run the -thing — so nobody noticed that the README's only verification step could not execute: -`panday-local --serve` spawns `llama-server` by name, the kit shipped no runner, and an air-gapped -machine cannot fetch one. The box promised "everything needed to run Panday" and could not answer -a prompt. +### A pattern worth naming, found three times in one run -That is now `xtask airgap --runner `, a README that adapts, and a builder that refuses a -runner name `panday-local` will never spawn. Review found two more doors into the same failure: a -packed `mistralrs-server` was not named in the verify command (the CLI defaults to llama-server), -and any basename was accepted. +Three protocol/manifest fields that read like controls and control nothing: -**The suite was made to fail before being trusted.** Sabotaging `install.sh` with -`getent hosts deb.debian.org` — a real DNS lookup absent from the hand-maintained -`NETWORK_COMMANDS` list — left the static guard blind (kit emitted, 27/27 unit tests green) while -the container suite went 3 of 5 red. That gap is the whole reason the container test exists, and -it is the argument for running it rather than adding another keyword to the list. +1. **`NetPolicy::allow`** — a non-empty allowlist did not narrow egress, it *removed* the network + namespace on Linux and emitted `(allow network-outbound)` plus inbound bind on macOS. Asking for + one host granted every host. Fixed (#36). +2. **`plugin.toml`'s `net:`** — parsed, validated, shown at the consent prompt, never reaching + `SandboxPolicy`. The user consented to something that could not happen. Fixed (#38). +3. **`CallMeta.task`** — docs/12 says "Callers that know, say"; `resolve()` never reads it, so a + caller declaring `Code` gets whatever the heuristic guesses. **Open** — honouring it changes + routing and needs its own commit. + +Each was individually latent. Together they say the manifest and policy layer has been written +ahead of the enforcement layer more than once, and nothing catches that class automatically. A lint +that fails when a policy field has no reader would have caught all three. ## 4. Known problems @@ -400,56 +397,45 @@ just bench # json-bench against a running gateway (Grok CLI OAuth is enough) ## 6. Suggested next steps, in order -**A previous version of this section said "nothing is laptop-buildable any more". That was -wrong**, and it was wrong because it repeated the blocked-list framing above instead of checking -the tree. Four milestones have closed since (M25.11, M22.5's container half, M11.10, M11.11), all -of them on this laptop, three of them previously described here as blocked. Do not invent rates or -p95s to make the list look shorter — but do not assume a milestone is blocked because this file -once said so. - -0. **M14.8 — the egress proxy.** The last numbered milestone with no hardware dependency, and the - only one left that is squarely buildable here. `docs/14-sandbox.md` records that the proxy "is - not built" and that "a per-domain allowlist needs the proxy component and is deferred with it"; - `docs/16-plugins.md` meanwhile lets `plugin.toml` declare `net: [...]` that nothing enforces. - A manifest field the sandbox cannot honour is the part worth fixing first, and worth stating in - docs/16 either way. - - Bigger than the last three: a real component, not another implementation behind an existing - trait. Scope it before starting. - -1. **Phase 1 dogfood (the builder, not an agent).** Use the agent on a real repo under `dev` and - decide whether you reach for it the next day. The fixture loop already passed live. This is the - real critical path — M19.4/M19.5/M19.7 all need transcript volume that only daily use produces. -2. **M22.5's last mile — one real disconnected box.** The container half shipped (#32): the kit - installs under `docker run --network none`, `models/` is exercised, and the support-boundary - table is written. What remains is an install on a genuinely disconnected machine following only - `INSTALL.md`, by someone who has not read this repo. The container proves the installer needs - no network; it does not prove the README is followable. - - Open and deliberately undecided: whether a released kit should default to packing an inference - runner. `xtask airgap --runner ` exists and the README adapts either way, but defaulting - means vendoring a third-party binary per architecture, with a licensing surface `cargo deny` - cannot see. Recorded in docs/18 M18.7. -3. **A host (M22.2 leftover / M22.3).** The deploy workflow is written and guarded on - `STAGING_DEPLOY_HOST`; set that secret plus `STAGING_SSH_KEY`, `STAGING_SMOKE_URL`, - `STAGING_DATABASE_URL` and it runs, with the smoke suite gating the deploy. Do not rent a VM - with the user's money unasked. -4. **KVM (M14.5–14.6, M22.4).** Two nodes, then measure fold latency. Do not invent p95s. -5. **Stripe (M17.4 leftover / Phase 3 exit).** Live key, then the HTTP client and signature check. - Do not add a Stripe crate without a key the user already set. -6. **Training (M19.3, then M19.5 / M19.7).** Labelled corpus that is *not* the 50 route-bench - prompts; GPU-hours; docs/19 go/no-go before GRPO spend. The `Classifier` trait and shadow - harness are the slot Model 1 drops into. Note the three acceptance gates are still prose, not - code — `eval.rs` has no cost dimension at all, so M19.5's gate is currently unimplementable. -7. **If growing agent-bench back toward 50 tasks**, take them from mined traffic (M19.4), not from - invention — and re-read §1.1 first. The twelve tasks removed were the destructive classes; they - are not coming back. +An autonomous run (2026-08-23/24, brief in `GOAL.prompt.md`, log in `RUN-REPORT.md`) worked the +laptop-doable list to exhaustion. What is left is genuinely gated on decisions, hardware, or you. + +0. **Answer the four questions at the top of `RUN-REPORT.md`.** Every one blocks work that is + otherwise ready. The first — M14.8 fail-closed vs proxy — is nearly done either way. +1. **Phase 1 dogfood (yours, not an agent's).** Use the agent on a real repo under `dev` and decide + whether you reach for it the next day. This is the critical path and always was: M19.4/M19.5/ + M19.7 need transcript volume that only daily use produces, and nine milestones of infrastructure + do not move it. +2. **`CallMeta.task` — decide and land.** docs/12 promises callers can declare a task class; + nothing reads the field. Honouring it changes routing, so it wants its own commit and a moment's + thought about what a wrong declaration should cost. +3. **M14.9's T3-remote and the no-egress contract.** `NetPolicy`'s doc says an empty allowlist means + "no egress at all"; `t3_remote.rs` does nothing to make that true on a hosted microVM. Either + make it refuse, or say in docs/14 that the tier cannot honour it. (That a CodeSandbox VM has + internet is inferred, not tested — no token, no live calls.) +4. **M22.5's last mile** — one physically disconnected box, installed from `INSTALL.md` by someone + who has not read this repo. +5. **A host (M22.3)**, **KVM (M22.4, M14.5–14.6)**, **Stripe (M17.4)**, **GPUs (M19.3/5/7)** — each + blocked on the thing named. The training *gates* are now executable (#40), so a model can be + judged the day one exists; do not fabricate a score in the meantime. ### Two known-flaky tests, neither fixed -- `panday-sandbox::t2_macos_escape::a_nonzero_exit_is_reported_not_swallowed` fails when the - machine is saturated: the jail's 30s wall clock elapses, the child is killed, and a killed - process reports no exit code — so `Some(3)` reads as `None`. Passes 5/5 in isolation. Seen while - a full gate, Docker and a workspace rebuild were competing. -- nextest reports `1 leaky` on `panday-platform` occasionally — a test leaving a handle open, - most likely an unclosed pool. Not a failure; CI is clean. +- `panday-sandbox::a_nonzero_exit_is_reported_not_swallowed` fails when the jail's 30s wall clock + elapses under load: the child is killed, a killed process reports no exit code, so `Some(3)` reads + as `None`. Passes 5/5 on a quiet machine. +- nextest reported `1 leaky` on `panday-platform` once and not in the four runs since. The only + detached write in the crate is `PgRouteAudit::record`'s `tokio::spawn`, which by design outlives + its caller — that is the plausible mechanism. `LedgerSink::record` awaits directly and is not it. + +### What this machine does to the test suite + +Gates ran **60 minutes to 3.5 hours** during the run, against a usual ~15, because another project +built alongside and UTM/QEMU held ~127% of a core for seven hours. Nothing was wrong with the code. +Three consequences worth knowing before blaming a change: + +- A `cargo test --workspace` **parent** at 0% CPU is normal — it waits on children. Judge a stall by + the child. +- `mcp_in_the_loop` hung for 50 minutes under contention and passes in **0.05s** on a quiet machine. +- Running `cargo test --workspace` twice in one script left the second copy holding the cargo lock + and blocking an unrelated run.