From b0b485abff802b79d238dd24a9fa4abd0713d3ee Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:11:10 +0800 Subject: [PATCH] fix(hopper): verify and repair the Linux embedding runtime (#5041) Verify the distribution Python embedding library during approved Linux Hopper setup, conditionally repair the matching Ubuntu dependency, and confirm that the library loads. Share fresh bounded observations with doctor, preserve unknown configuration and caller cancellation, and join probe exit through the existing process supervisor. Validate clean Ubuntu installation, normal-user real Hopper CLI/MCP workflows, independent ELF/SQLite oracles, cancellation and cleanup regressions, 154 focused checks and all 40 current-head CI checks. Preserve measured artifact identity, failed attempts and local host-memory guard limitations. Closes #5015. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> --- docs/installation.md | 14 + experiments/analysis-efficiency/README.md | 4 + .../hopper-linux-python-runtime.json | 2270 +++++++++++++++++ .../hopper-linux-python-runtime.md | 203 ++ src/application/LinuxHopper.test.ts | 40 + src/application/LinuxHopper.ts | 219 +- .../LinuxHopperPackageRuntime.test.ts | 264 ++ src/application/LinuxHopperPython.test.ts | 257 ++ src/application/LinuxHopperPython.ts | 157 ++ src/application/SetupHost.ts | 7 +- src/application/SetupInstallFailure.ts | 4 +- src/application/SetupPlan.ts | 2 +- src/doctorRuntime.test.ts | 191 +- src/doctorRuntime.ts | 81 +- 14 files changed, 3653 insertions(+), 60 deletions(-) create mode 100644 experiments/analysis-efficiency/hopper-linux-python-runtime.json create mode 100644 experiments/analysis-efficiency/hopper-linux-python-runtime.md create mode 100644 src/application/LinuxHopperPackageRuntime.test.ts create mode 100644 src/application/LinuxHopperPython.test.ts create mode 100644 src/application/LinuxHopperPython.ts diff --git a/docs/installation.md b/docs/installation.md index f23fde01b4..7f4cb13422 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -599,6 +599,20 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes `sudo`. `rea doctor --provider hopper --json` reports the selected strategy and both host and effective mount facts. +Hopper also embeds Python; the Python executable and standard library alone +are insufficient on Ubuntu. Approved setup checks the system interpreter +at `/usr/bin/python3` and, when necessary, installs its matching +`libpython3.X` package, then verifies that its shared library can load. +APT resolves the concrete package (for example, `libpython3.12t64` on Ubuntu +24.04). Doctor includes these fresh observations in the `embedded_python` +details of its `hopper-demo-runtime` check, alongside the private-display +diagnostic. Failed inspections remain unknown rather than proving a library +absent. Run `rea setup --yes --install-hopper` to repair a supported install. +This prerequisite check does not launch the bridge or validate a custom Python +interpreter configured inside Hopper; that interpreter needs its own matching +shared runtime. Custom installation prefixes or shared-library ABIs require +manual dependency repair. + When the package-manager step fails, setup reports the failing command and its captured output, so a package conflict is distinguishable from an authorization failure. On CachyOS, an unversioned compatibility package such as diff --git a/experiments/analysis-efficiency/README.md b/experiments/analysis-efficiency/README.md index e014521fdc..5330f13574 100644 --- a/experiments/analysis-efficiency/README.md +++ b/experiments/analysis-efficiency/README.md @@ -4,6 +4,10 @@ For Hopper literal string queries, see the [decoded-record checkpoint](hopper-st It measures record reuse through real Linux CLI/MCP workflows, preserving native recognition gaps, guard-stopped attempts and negative whole-task costs. +The [Linux embedding-runtime follow-up](hopper-linux-python-runtime.md) checks +actual clean setup, dependency repair and normal-user analysis, retaining +failed attempts and distinguishing prerequisite correctness from speedups. + For JEB on Linux, see the [CE native audit](jeb-linux-native.md). It qualifies the direct headless launch route and measures selected function reads through real CLI and production stdio MCP, preserving timeout attempts and the limits diff --git a/experiments/analysis-efficiency/hopper-linux-python-runtime.json b/experiments/analysis-efficiency/hopper-linux-python-runtime.json new file mode 100644 index 0000000000..90ad595ac1 --- /dev/null +++ b/experiments/analysis-efficiency/hopper-linux-python-runtime.json @@ -0,0 +1,2270 @@ +{ + "schema_version": 1, + "issue": 5015, + "purpose": "Embedding dependency correctness and bounded real-provider qualification; no equivalent-task speedup claim", + "baseline_revision": "093ce695f0a7c3407c7fde520ed8b20c0981b408", + "earlier_prototype_runtime_bridge_manifest_sha256": "589cbe956e89b9bd794eb5ca43f5c5f70dabe33734176f42c4fd76e1da1a7f22", + "accounting": "Native includes guardian and observer at payload exit. Controller snapshots before final exit are separate and partial; sum of separate peaks is not simultaneous memory. Final removed-scope counters, Docker daemon/containerd/system-manager costs and actual model usage are unknown. Aborted observations remain null. Admission is excluded from stage elapsed and recorded separately. No zero-host-OOM claim.", + "resource_bounds": { + "native_memory_hard_bytes": 6442450944, + "native_memory_early_stop_bytes": 5368709120, + "controller_memory_hard_bytes": 536870912, + "host_job_memory_hard_bytes": 8589934592, + "host_job_memory_early_stop_bytes": 7516192768, + "swap_bytes": 0, + "cpu_cores_per_heavy_job": 1, + "native_task_limit": 128, + "controller_task_limit": 64, + "host_available_admission_minimum_bytes": 17179869184, + "host_available_abort_bytes": 8589934592 + }, + "native_stages": [ + { + "stage": "docker-baseline-bootstrap", + "exit": 0, + "abort": null, + "admission_seconds": 1.300009898841381e-6, + "elapsed_seconds": 8.403334519942291, + "native_cpu_seconds_at_payload_exit": 0.265018, + "native_peak_bytes_at_payload_exit": 33669120, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.380374, + "controller_peak_bytes_before_exit": 20180992, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-baseline-setup-short1", + "exit": 1, + "abort": null, + "admission_seconds": 9.014807273051701, + "elapsed_seconds": 5.467341389972717, + "native_cpu_seconds_at_payload_exit": 3.798731, + "native_peak_bytes_at_payload_exit": 411799552, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.400512, + "controller_peak_bytes_before_exit": 22310912, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-baseline-setup-short2", + "exit": 137, + "abort": "host_available_memory_early_stop", + "admission_seconds": 11.01192738302052, + "elapsed_seconds": 71.6523128489498, + "native_cpu_seconds_at_payload_exit": null, + "native_peak_bytes_at_payload_exit": null, + "payload_cleanup_verified": null, + "guardian_only_before_stop": null, + "native_events_at_payload_exit": null, + "controller_cpu_seconds_before_exit": 0.691079, + "controller_peak_bytes_before_exit": 31379456, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-bootstrap-strict-1", + "exit": 0, + "abort": null, + "admission_seconds": 10.003992603975348, + "elapsed_seconds": 11.063847957062535, + "native_cpu_seconds_at_payload_exit": 7.823089, + "native_peak_bytes_at_payload_exit": 427913216, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.444222, + "controller_peak_bytes_before_exit": 22913024, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-candidate-bootstrap-1", + "exit": 0, + "abort": null, + "admission_seconds": 39.04051249101758, + "elapsed_seconds": 10.573263339931145, + "native_cpu_seconds_at_payload_exit": 7.058789, + "native_peak_bytes_at_payload_exit": 434565120, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.40723, + "controller_peak_bytes_before_exit": 23273472, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-candidate-doctor-ready-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.2799864634871483e-6, + "elapsed_seconds": 6.159401457989588, + "native_cpu_seconds_at_payload_exit": 5.141644, + "native_peak_bytes_at_payload_exit": 588095488, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.365368, + "controller_peak_bytes_before_exit": 22327296, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-candidate-installed-state-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.2600794434547424e-6, + "elapsed_seconds": 1.079862384009175, + "native_cpu_seconds_at_payload_exit": 0.261479, + "native_peak_bytes_at_payload_exit": 32526336, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.410363, + "controller_peak_bytes_before_exit": 22564864, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-candidate-mcp-control-1", + "exit": 0, + "abort": null, + "admission_seconds": 2.0599691197276115e-6, + "elapsed_seconds": 26.48699402390048, + "native_cpu_seconds_at_payload_exit": 25.128943, + "native_peak_bytes_at_payload_exit": 1265922048, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.468137, + "controller_peak_bytes_before_exit": 22708224, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-candidate-setup-fresh-1", + "exit": 0, + "abort": null, + "admission_seconds": 2.0299339666962624e-6, + "elapsed_seconds": 89.97979429503903, + "native_cpu_seconds_at_payload_exit": 46.322647, + "native_peak_bytes_at_payload_exit": 1611173888, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.82639, + "controller_peak_bytes_before_exit": 22859776, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-clean-bootstrap-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.4899997040629387e-6, + "elapsed_seconds": 13.049777403008193, + "native_cpu_seconds_at_payload_exit": 6.903585, + "native_peak_bytes_at_payload_exit": 428007424, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.398052, + "controller_peak_bytes_before_exit": 22368256, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-clean-installed-state-1", + "exit": 0, + "abort": null, + "admission_seconds": 2.049957402050495e-6, + "elapsed_seconds": 0.992557693971321, + "native_cpu_seconds_at_payload_exit": 0.257221, + "native_peak_bytes_at_payload_exit": 34516992, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.387169, + "controller_peak_bytes_before_exit": 22360064, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-clean-libpython-intervention-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.2399395927786827e-6, + "elapsed_seconds": 5.01243132899981, + "native_cpu_seconds_at_payload_exit": 1.924607, + "native_peak_bytes_at_payload_exit": 183316480, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.394263, + "controller_peak_bytes_before_exit": 24350720, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-clean-mcp-baseline-1", + "exit": 1, + "abort": null, + "admission_seconds": 1.3799872249364853e-6, + "elapsed_seconds": 49.54120984498877, + "native_cpu_seconds_at_payload_exit": 24.158373, + "native_peak_bytes_at_payload_exit": 1344757760, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.537938, + "controller_peak_bytes_before_exit": 22429696, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-clean-mcp-intervention-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.7100246623158455e-6, + "elapsed_seconds": 24.791497931000777, + "native_cpu_seconds_at_payload_exit": 23.608192, + "native_peak_bytes_at_payload_exit": 1288110080, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.464903, + "controller_peak_bytes_before_exit": 24322048, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-clean-setup-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.7400598153471947e-6, + "elapsed_seconds": 75.35331558506005, + "native_cpu_seconds_at_payload_exit": 39.002301, + "native_peak_bytes_at_payload_exit": 1463148544, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.614015, + "controller_peak_bytes_before_exit": 22417408, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-interrupted-install-state-1", + "exit": 0, + "abort": null, + "admission_seconds": 5.002039189916104, + "elapsed_seconds": 1.6014929889934137, + "native_cpu_seconds_at_payload_exit": 0.263304, + "native_peak_bytes_at_payload_exit": 70561792, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.448111, + "controller_peak_bytes_before_exit": 58122240, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-network-gateway-2", + "exit": 0, + "abort": null, + "admission_seconds": 2.769986167550087e-6, + "elapsed_seconds": 3.7278700680471957, + "native_cpu_seconds_at_payload_exit": 0.445586, + "native_peak_bytes_at_payload_exit": 101060608, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.361493, + "controller_peak_bytes_before_exit": 24674304, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-network-probe-1", + "exit": 1, + "abort": null, + "admission_seconds": 67.05681774904951, + "elapsed_seconds": 1.320145520963706, + "native_cpu_seconds_at_payload_exit": 0.317189, + "native_peak_bytes_at_payload_exit": 78946304, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.425613, + "controller_peak_bytes_before_exit": 57966592, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-cli-control-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.208973117172718e-6, + "elapsed_seconds": 9.003808271954767, + "native_cpu_seconds_at_payload_exit": 7.850667, + "native_peak_bytes_at_payload_exit": 638164992, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.383686, + "controller_peak_bytes_before_exit": 22343680, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-cli-sqlite-1", + "exit": 0, + "abort": null, + "admission_seconds": 2.020038664340973e-6, + "elapsed_seconds": 86.7183938639937, + "native_cpu_seconds_at_payload_exit": 83.058079, + "native_peak_bytes_at_payload_exit": 799977472, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.797757, + "controller_peak_bytes_before_exit": 23220224, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-doctor-missing-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.9898870959877968e-6, + "elapsed_seconds": 6.2618385560344905, + "native_cpu_seconds_at_payload_exit": 4.252893, + "native_peak_bytes_at_payload_exit": 723087360, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.329627, + "controller_peak_bytes_before_exit": 58081280, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-doctor-ready-1", + "exit": 0, + "abort": null, + "admission_seconds": 9.899958968162537e-7, + "elapsed_seconds": 4.2471351330168545, + "native_cpu_seconds_at_payload_exit": 3.714773, + "native_peak_bytes_at_payload_exit": 584744960, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.269099, + "controller_peak_bytes_before_exit": 22298624, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-installed-state-1", + "exit": 0, + "abort": null, + "admission_seconds": 7.909256964921951e-7, + "elapsed_seconds": 0.7802283560158685, + "native_cpu_seconds_at_payload_exit": 0.20204, + "native_peak_bytes_at_payload_exit": 37187584, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.284258, + "controller_peak_bytes_before_exit": 24145920, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-mcp-control-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.0200310498476028e-6, + "elapsed_seconds": 20.570275390986353, + "native_cpu_seconds_at_payload_exit": 18.835501, + "native_peak_bytes_at_payload_exit": 1423052800, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.359416, + "controller_peak_bytes_before_exit": 22638592, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-mcp-sqlite-1", + "exit": 1, + "abort": null, + "admission_seconds": 2.020038664340973e-6, + "elapsed_seconds": 49.71109775605146, + "native_cpu_seconds_at_payload_exit": 48.482308, + "native_peak_bytes_at_payload_exit": 1404461056, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.544033, + "controller_peak_bytes_before_exit": 22274048, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-mcp-sqlite-default-1", + "exit": 1, + "abort": null, + "admission_seconds": 104.08551430900116, + "elapsed_seconds": 85.3799227870768, + "native_cpu_seconds_at_payload_exit": 83.978334, + "native_peak_bytes_at_payload_exit": 1500237824, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.684198, + "controller_peak_bytes_before_exit": 22798336, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-mcp-sqlite-master-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.2700911611318588e-6, + "elapsed_seconds": 89.17912931507453, + "native_cpu_seconds_at_payload_exit": 87.658862, + "native_peak_bytes_at_payload_exit": 1447657472, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.693465, + "controller_peak_bytes_before_exit": 22736896, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-remove-runtime-1", + "exit": 0, + "abort": null, + "admission_seconds": 173.9988398249261, + "elapsed_seconds": 1.124993653036654, + "native_cpu_seconds_at_payload_exit": 0.247663, + "native_peak_bytes_at_payload_exit": 34467840, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 9.909766, + "controller_peak_bytes_before_exit": 63827968, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "docker-repair-setup-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.3799872249364853e-6, + "elapsed_seconds": 16.781416470068507, + "native_cpu_seconds_at_payload_exit": 9.115502, + "native_peak_bytes_at_payload_exit": 803631104, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.398717, + "controller_peak_bytes_before_exit": 24403968, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-bootstrap-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.3899989426136017e-6, + "elapsed_seconds": 11.713265593978576, + "native_cpu_seconds_at_payload_exit": 7.175923, + "native_peak_bytes_at_payload_exit": 434622464, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.399999, + "controller_peak_bytes_before_exit": 22335488, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-doctor-ready-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.650070771574974e-6, + "elapsed_seconds": 6.03493693494238, + "native_cpu_seconds_at_payload_exit": 5.159615, + "native_peak_bytes_at_payload_exit": 588181504, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.364336, + "controller_peak_bytes_before_exit": 22241280, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-installed-state-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.2899981811642647e-6, + "elapsed_seconds": 0.9451459859265015, + "native_cpu_seconds_at_payload_exit": 0.250318, + "native_peak_bytes_at_payload_exit": 34668544, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.36156, + "controller_peak_bytes_before_exit": 22343680, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-normal-cli-control-1", + "exit": 0, + "abort": null, + "admission_seconds": 86.03516408696305, + "elapsed_seconds": 8.980948965065181, + "native_cpu_seconds_at_payload_exit": 7.830533, + "native_peak_bytes_at_payload_exit": 643313664, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.405383, + "controller_peak_bytes_before_exit": 22265856, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-normal-cli-sqlite-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.7399434000253677e-6, + "elapsed_seconds": 70.20744847296737, + "native_cpu_seconds_at_payload_exit": 68.704228, + "native_peak_bytes_at_payload_exit": 819589120, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.587324, + "controller_peak_bytes_before_exit": 22413312, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-normal-mcp-control-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.8399441614747047e-6, + "elapsed_seconds": 24.43435979797505, + "native_cpu_seconds_at_payload_exit": 23.30478, + "native_peak_bytes_at_payload_exit": 1253810176, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.473695, + "controller_peak_bytes_before_exit": 19873792, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-normal-mcp-sqlite-1", + "exit": 0, + "abort": null, + "admission_seconds": 20.007159981993027, + "elapsed_seconds": 92.50391822995152, + "native_cpu_seconds_at_payload_exit": 90.138292, + "native_peak_bytes_at_payload_exit": 1580445696, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.695817, + "controller_peak_bytes_before_exit": 28626944, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-normal-user-prepare-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.2599630281329155e-6, + "elapsed_seconds": 1.1131400050362572, + "native_cpu_seconds_at_payload_exit": 0.341054, + "native_peak_bytes_at_payload_exit": 126337024, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.336317, + "controller_peak_bytes_before_exit": 22290432, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + }, + { + "stage": "final-setup-1", + "exit": 0, + "abort": null, + "admission_seconds": 1.8799910321831703e-6, + "elapsed_seconds": 80.7387062109774, + "native_cpu_seconds_at_payload_exit": 40.40383, + "native_peak_bytes_at_payload_exit": 1449742336, + "payload_cleanup_verified": true, + "guardian_only_before_stop": true, + "native_events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "controller_cpu_seconds_before_exit": 0.642531, + "controller_peak_bytes_before_exit": 22380544, + "source_sha256": { + "docker-profile.py": "efb29b17127d4954e309c71fd703d86d3c02d321ce3bfcf84262636967c7e59a", + "docker-observer.py": "a5df228ab34a60a68f040d944ff79af15b7696e910c60e9cfa904e5108aa5390", + "docker-guardian.py": "9688bc82fb40cddb8836cba9c4b536cb573252feeb5b601ea3461d3358a8ac11" + } + } + ], + "host_checks": [ + { + "name": "build-candidate-1", + "abort": null, + "payload_exit": 2, + "elapsed_seconds": 114.216, + "sampled_peak_bytes": 3103449088, + "cpu_seconds_at_payload_exit": 110.437519, + "kernel_peak_bytes_at_payload_exit": 3108429824, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "build-candidate-2", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 125.581, + "sampled_peak_bytes": 2991472640, + "cpu_seconds_at_payload_exit": 122.476721, + "kernel_peak_bytes_at_payload_exit": 2996453376, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "build-final-1", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 124.196, + "sampled_peak_bytes": 3036753920, + "cpu_seconds_at_payload_exit": 120.62794, + "kernel_peak_bytes_at_payload_exit": 3040948224, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "focused-final-1", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 32.725, + "sampled_peak_bytes": 1166471168, + "cpu_seconds_at_payload_exit": 30.784558, + "kernel_peak_bytes_at_payload_exit": 1174876160, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "focused-final-2", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 38.961, + "sampled_peak_bytes": 1100095488, + "cpu_seconds_at_payload_exit": 37.152523, + "kernel_peak_bytes_at_payload_exit": 1100881920, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "lint-final-1", + "abort": null, + "payload_exit": 1, + "elapsed_seconds": 4.672, + "sampled_peak_bytes": 1055670272, + "cpu_seconds_at_payload_exit": 3.447286, + "kernel_peak_bytes_at_payload_exit": 1055670272, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "lint-final-2", + "abort": null, + "payload_exit": 1, + "elapsed_seconds": 6.72, + "sampled_peak_bytes": 1012363264, + "cpu_seconds_at_payload_exit": 5.9091, + "kernel_peak_bytes_at_payload_exit": 1012363264, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "lint-final-3", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 20.576, + "sampled_peak_bytes": 1008173056, + "cpu_seconds_at_payload_exit": 16.972214, + "kernel_peak_bytes_at_payload_exit": 1008173056, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "test-discovery-final-1", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 1.415, + "sampled_peak_bytes": 147243008, + "cpu_seconds_at_payload_exit": 1.183695, + "kernel_peak_bytes_at_payload_exit": 158777344, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "package-final-1", + "abort": "host_available_memory_early_stop", + "payload_exit": null, + "elapsed_seconds": 336.196, + "sampled_peak_bytes": 1742426112, + "cpu_seconds_at_payload_exit": null, + "kernel_peak_bytes_at_payload_exit": null, + "payload_cleanup_verified": null, + "events_at_payload_exit": null + }, + { + "name": "token-estimates-1", + "abort": null, + "payload_exit": 0, + "elapsed_seconds": 8.441, + "sampled_peak_bytes": 159526912, + "cpu_seconds_at_payload_exit": 8.08087, + "kernel_peak_bytes_at_payload_exit": 164552704, + "payload_cleanup_verified": true, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "name": "check-fast-1", + "abort": "host_available_memory_early_stop", + "payload_exit": null, + "elapsed_seconds": 179.96, + "sampled_peak_bytes": 4135276544, + "cpu_seconds_at_payload_exit": null, + "kernel_peak_bytes_at_payload_exit": null, + "payload_cleanup_verified": null, + "events_at_payload_exit": null + }, + { + "name": "check-fast-final-1", + "abort": "host_available_memory_early_stop", + "payload_exit": null, + "elapsed_seconds": 181.571, + "sampled_peak_bytes": 4203474944, + "cpu_seconds_at_payload_exit": null, + "kernel_peak_bytes_at_payload_exit": null, + "payload_cleanup_verified": null, + "events_at_payload_exit": null + } + ], + "workflow_results": [ + { + "run": "repair-mcp-control-1", + "status": "passed", + "elapsed_ms": 17762.418623999998, + "target_sha256": "1a6f55303712e096ff5196c7fd6e8acf75473fbf0f7467d7b315f96f50b3f841", + "target_unchanged": true, + "match_count": 1, + "recorded_calls": 13, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 9035.523665, + "reply_bytes": 677, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 433.60833099999945, + "reply_bytes": 121777, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 3946.667469000002, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-0", + "ms": 27.089098000000376, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-1", + "ms": 4.280088999999862, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-2", + "ms": 4.2711510000008275, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-3", + "ms": 4.646459999999934, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-4", + "ms": 4.9889600000024075, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-5", + "ms": 5.152068999999756, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-absent", + "ms": 3.2672310000016296, + "reply_bytes": 4445, + "isError": false + }, + { + "name": "list_procedures", + "phase": "list_procedures", + "ms": 10.046900000001187, + "reply_bytes": 4057, + "isError": false + }, + { + "name": "procedure_pseudo_code", + "phase": "decompile", + "ms": 12.081679000002623, + "reply_bytes": 3145, + "isError": false + }, + { + "name": "close_binary", + "phase": "close", + "ms": 67.20499700000073, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "repair-mcp-sqlite-1", + "status": "failed", + "elapsed_ms": 46525.535972, + "target_sha256": "2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8", + "recorded_calls": 4, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 10745.91748, + "reply_bytes": 693, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 299.025486999999, + "reply_bytes": 121793, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 30212.09512, + "reply_bytes": 2208, + "isError": true + }, + { + "name": "close_binary", + "phase": "cleanup-close", + "ms": 9.240499999999884, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "repair-mcp-sqlite-default-1", + "status": "failed", + "elapsed_ms": 82147.554584, + "target_sha256": "2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8", + "recorded_calls": 4, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 10151.484841000001, + "reply_bytes": 693, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 302.50478999999905, + "reply_bytes": 121793, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 66325.553732, + "reply_bytes": 4405, + "isError": false + }, + { + "name": "close_binary", + "phase": "cleanup-close", + "ms": 135.95772599999327, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "repair-mcp-sqlite-master-1", + "status": "passed", + "elapsed_ms": 85819.30617299999, + "target_sha256": "2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8", + "target_unchanged": true, + "match_count": 19, + "inventory_count": 1437, + "recorded_calls": 16, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 11383.087792, + "reply_bytes": 693, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 198.7831589999987, + "reply_bytes": 121793, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 68312.421416, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-0", + "ms": 14.93857699999353, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-1", + "ms": 15.416408000004594, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-2", + "ms": 11.655997999987449, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-3", + "ms": 12.449017999999342, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-4", + "ms": 13.535647999990033, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-5", + "ms": 10.359918000001926, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "list_strings", + "phase": "validation-full-inventory", + "ms": 189.18466300000728, + "reply_bytes": 675670, + "isError": false + }, + { + "name": "search_strings", + "phase": "validation-broad", + "ms": 13.404458999997587, + "reply_bytes": 50237, + "isError": false + }, + { + "name": "search_strings", + "phase": "validation-regex", + "ms": 128.99664100000518, + "reply_bytes": 20571, + "isError": false + }, + { + "name": "search_strings", + "phase": "validation-case-insensitive", + "ms": 9.334688999995706, + "reply_bytes": 20577, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-absent", + "ms": 5.647669999991194, + "reply_bytes": 4477, + "isError": false + }, + { + "name": "procedure_pseudo_code", + "phase": "decompile", + "ms": 14.874846999999136, + "reply_bytes": 3108, + "isError": false + }, + { + "name": "close_binary", + "phase": "close", + "ms": 120.6108130000066, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "repair-cli-control-1", + "status": "passed", + "elapsed_ms": 8005.314384, + "target_sha256": "1a6f55303712e096ff5196c7fd6e8acf75473fbf0f7467d7b315f96f50b3f841", + "target_unchanged": true, + "facts_and_identity_match_mcp": true, + "reply_bytes": 2269 + }, + { + "run": "repair-cli-sqlite-1", + "status": "passed", + "elapsed_ms": 85195.669735, + "target_sha256": "2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8", + "target_unchanged": true, + "facts_and_identity_match_mcp": true, + "reply_bytes": 9843 + }, + { + "run": "candidate-mcp-control-1", + "status": "passed", + "elapsed_ms": 22937.53824, + "target_sha256": "1a6f55303712e096ff5196c7fd6e8acf75473fbf0f7467d7b315f96f50b3f841", + "target_unchanged": true, + "match_count": 1, + "recorded_calls": 13, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 11871.366794000001, + "reply_bytes": 677, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 207.82457700000305, + "reply_bytes": 121777, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 5257.534409, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-0", + "ms": 10.415859000000637, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-1", + "ms": 9.798678999999538, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-2", + "ms": 7.722019000000728, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-3", + "ms": 8.912458999999217, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-4", + "ms": 8.446438999999373, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-5", + "ms": 7.805339000002277, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-absent", + "ms": 8.597968999998557, + "reply_bytes": 4445, + "isError": false + }, + { + "name": "list_procedures", + "phase": "list_procedures", + "ms": 16.08401800000138, + "reply_bytes": 4057, + "isError": false + }, + { + "name": "procedure_pseudo_code", + "phase": "decompile", + "ms": 17.639328000001115, + "reply_bytes": 3145, + "isError": false + }, + { + "name": "close_binary", + "phase": "close", + "ms": 99.17012900000191, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "final-normal-mcp-control-1", + "status": "passed", + "elapsed_ms": 21234.686771, + "target_sha256": "1a6f55303712e096ff5196c7fd6e8acf75473fbf0f7467d7b315f96f50b3f841", + "target_unchanged": true, + "match_count": 1, + "recorded_calls": 13, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 10507.004876, + "reply_bytes": 677, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 293.5565290000013, + "reply_bytes": 121777, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 5128.119548000002, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-0", + "ms": 9.999560000000201, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-1", + "ms": 9.945700000000215, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-2", + "ms": 8.527290000001813, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-3", + "ms": 9.220509999999194, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-4", + "ms": 11.457610000001296, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-5", + "ms": 9.264670000000478, + "reply_bytes": 4851, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-absent", + "ms": 8.238879999997152, + "reply_bytes": 4445, + "isError": false + }, + { + "name": "list_procedures", + "phase": "list_procedures", + "ms": 18.254070000002685, + "reply_bytes": 4057, + "isError": false + }, + { + "name": "procedure_pseudo_code", + "phase": "decompile", + "ms": 16.77081999999791, + "reply_bytes": 3145, + "isError": false + }, + { + "name": "close_binary", + "phase": "close", + "ms": 84.85122899999988, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "final-normal-mcp-sqlite-1", + "status": "passed", + "elapsed_ms": 89125.24830600001, + "target_sha256": "2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8", + "target_unchanged": true, + "match_count": 19, + "inventory_count": 1437, + "recorded_calls": 16, + "calls": [ + { + "name": "open_binary", + "phase": "open", + "ms": 11124.027717, + "reply_bytes": 693, + "isError": false + }, + { + "name": "binary_session", + "phase": "binary_session", + "ms": 557.1847369999996, + "reply_bytes": 121793, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-cold", + "ms": 71274.42624900001, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-0", + "ms": 13.8874599999981, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-1", + "ms": 15.099549999998999, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-2", + "ms": 11.713569999992615, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-3", + "ms": 12.387959999992745, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-4", + "ms": 12.126250000001164, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-warm-5", + "ms": 10.05956999999762, + "reply_bytes": 20575, + "isError": false + }, + { + "name": "list_strings", + "phase": "validation-full-inventory", + "ms": 199.86688899999717, + "reply_bytes": 675670, + "isError": false + }, + { + "name": "search_strings", + "phase": "validation-broad", + "ms": 14.669269999998505, + "reply_bytes": 50237, + "isError": false + }, + { + "name": "search_strings", + "phase": "validation-regex", + "ms": 124.15337899999577, + "reply_bytes": 20571, + "isError": false + }, + { + "name": "search_strings", + "phase": "validation-case-insensitive", + "ms": 9.603839999996126, + "reply_bytes": 20577, + "isError": false + }, + { + "name": "search_strings", + "phase": "search-absent", + "ms": 5.5380110000114655, + "reply_bytes": 4477, + "isError": false + }, + { + "name": "procedure_pseudo_code", + "phase": "decompile", + "ms": 15.798209000000497, + "reply_bytes": 3108, + "isError": false + }, + { + "name": "close_binary", + "phase": "close", + "ms": 120.54429000000528, + "reply_bytes": 169, + "isError": false + } + ] + }, + { + "run": "final-normal-cli-control-1", + "status": "passed", + "elapsed_ms": 7954.587291000001, + "target_sha256": "1a6f55303712e096ff5196c7fd6e8acf75473fbf0f7467d7b315f96f50b3f841", + "target_unchanged": true, + "facts_and_identity_match_mcp": true, + "reply_bytes": 2269 + }, + { + "run": "final-normal-cli-sqlite-1", + "status": "passed", + "elapsed_ms": 69166.50034, + "target_sha256": "2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8", + "target_unchanged": true, + "facts_and_identity_match_mcp": true, + "reply_bytes": 9843 + } + ], + "token_estimates": { + "tokenizer": "tiktoken", + "version": "0.12.0", + "regex_version": "2026.9.29", + "encoding": "cl100k_base", + "scope": "Offline serialization estimates, one full catalog, named tool arguments and raw replies once each; replies may contain both text and structured copies. CLI rows include selected arguments and stdout. Actual model usage, initialization/protocol framing, prompts, host schema filtering/replay, rendering, independent oracle context and pre-transcript/in-flight failures are unmeasured. Failed and incomplete attempts retained as lower bounds. Provider qualification, not an agent investigation benchmark.", + "runs": [ + { + "run": "candidate-mcp-control-1", + "status": "passed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 296, + "raw_reply_tokens": 39506, + "recorded_calls": 13, + "transcript_sha256": "79021e7505c2a78164e0eb64493d3dda4cadbb3749d6ee06b9351d323fd3efb2", + "raw_total_tokens": 845132 + }, + { + "run": "clean-mcp-baseline-1", + "status": "failed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 86, + "raw_reply_tokens": 27509, + "recorded_calls": 4, + "transcript_sha256": "978133ca9a8563e7b6100cd2210bd1d5b8a06cb13ea791354bca489703744eab", + "raw_total_tokens": 832925 + }, + { + "run": "clean-mcp-intervention-1", + "status": "passed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 296, + "raw_reply_tokens": 39496, + "recorded_calls": 13, + "transcript_sha256": "81c946d5d4c25f9ef70b3532a5bf69761d9b83a6671bd42385fbad8f2bc5a8e8", + "raw_total_tokens": 845122 + }, + { + "run": "final-normal-mcp-control-1", + "status": "passed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 296, + "raw_reply_tokens": 39500, + "recorded_calls": 13, + "transcript_sha256": "5902bd5f26fe584055ae21e56f201878c4088561258a743c47d811040d9f4f73", + "raw_total_tokens": 845126 + }, + { + "run": "final-normal-mcp-sqlite-1", + "status": "passed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 348, + "raw_reply_tokens": 258896, + "recorded_calls": 16, + "transcript_sha256": "3fbe3f107d25da65d9d73d97d66fd38bf444a420963100e610e024224c3ac5bf", + "raw_total_tokens": 1064574 + }, + { + "run": "repair-mcp-control-1", + "status": "passed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 296, + "raw_reply_tokens": 39500, + "recorded_calls": 13, + "transcript_sha256": "c7d1875e7fed2603d7ea597249028f9228ddca1ba600c933fa63ec1d5a46a83c", + "raw_total_tokens": 845126 + }, + { + "run": "repair-mcp-sqlite-1", + "status": "failed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 89, + "raw_reply_tokens": 27499, + "recorded_calls": 4, + "transcript_sha256": "7b408b97d4f367779dd0a8c73db5967e2fbe0736d59bdbfc0ace8728413d6371", + "raw_total_tokens": 832918 + }, + { + "run": "repair-mcp-sqlite-default-1", + "status": "failed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 89, + "raw_reply_tokens": 28128, + "recorded_calls": 4, + "transcript_sha256": "36a3efb641fca629facdb07f7ff9fd22d27f3a36b54efc989329e65e480d0018", + "raw_total_tokens": 833547 + }, + { + "run": "repair-mcp-sqlite-master-1", + "status": "passed", + "coverage": "recorded_exchanges", + "catalog_tokens": 805330, + "named_request_tokens": 348, + "raw_reply_tokens": 258892, + "recorded_calls": 16, + "transcript_sha256": "7f8cb86780f06e6f29891d236ba020e10c327492d16c2a585fd9c5d087e0d7ca", + "raw_total_tokens": 1064570 + }, + { + "run": "final-normal-cli-control-1", + "status": "passed", + "interface": "CLI", + "selected_arguments_tokens": 75, + "stdout_tokens": 620, + "raw_total_tokens": 695 + }, + { + "run": "final-normal-cli-sqlite-1", + "status": "passed", + "interface": "CLI", + "selected_arguments_tokens": 79, + "stdout_tokens": 2826, + "raw_total_tokens": 2905 + }, + { + "run": "repair-cli-control-1", + "status": "passed", + "interface": "CLI", + "selected_arguments_tokens": 75, + "stdout_tokens": 620, + "raw_total_tokens": 695 + }, + { + "run": "repair-cli-sqlite-1", + "status": "passed", + "interface": "CLI", + "selected_arguments_tokens": 79, + "stdout_tokens": 2826, + "raw_total_tokens": 2905 + } + ] + }, + "native_validation_source_commit": "0c70be0030f6bb821c4292c4f5b7f776b365c8ec", + "native_validation_source_base_revision": "23f984b1a884baefc3c44e4ab697f6eff61dffb6", + "native_validation_runtime_bridge_manifest": { + "tree_sha256": "8800edc8e70965ae2132e57ca6d999ba276503f27f1045b33305c83cd0e2ff2a", + "total_bytes": 26537393, + "base_main": "23f984b1a884baefc3c44e4ab697f6eff61dffb6", + "scope": "All regular dist and bridge files; canonical sorted-key compact JSON manifest hash" + }, + "native_validation_runtime_bridge_file_count": 1733, + "cancellation_followup_scope": "Later doctor cancellation propagation changes only the cancelled-probe branch. Native success measurements belong to pinned commit0c70be0030; follow-up boundary/CI validation is recorded on the PR. No final-head native rerun is claimed." +} diff --git a/experiments/analysis-efficiency/hopper-linux-python-runtime.md b/experiments/analysis-efficiency/hopper-linux-python-runtime.md new file mode 100644 index 0000000000..ce98353d8e --- /dev/null +++ b/experiments/analysis-efficiency/hopper-linux-python-runtime.md @@ -0,0 +1,203 @@ +# Linux Hopper embedding-runtime qualification + +This follow-up to the [string-record audit](hopper-string-records.md) resolves +[#5015](https://github.com/morluto/rea/issues/5015): a successful package install +could leave Hopper unable to start its Python bridge. This is a prerequisite +correctness fix. It does not establish faster equivalent investigations. + +## Reproduction and owning correction + +Baseline REA `093ce695f0a7c3407c7fde520ed8b20c0981b408` ran the actual public +`rea setup --yes --install-hopper --format json` in a fresh Ubuntu Base root, +with full Linux UID/GID support, successful strict APT metadata refresh, +default APT recommendations and unchanged vendor post-install scripts. Setup +reported `ready` and `installed_hopper`; `dpkg --audit` was empty. Python 3.12 +was configured, but its reported `/usr/lib/x86_64-linux-gnu/libpython3.12.so.1.0` +was absent. The signed Hopper package did not declare this dependency. + +An actual public MCP control-ELF workflow then timed out at its selected +30-second startup budget. Installing only `libpython3.12` resolved to +`libpython3.12t64` version `3.12.3-1ubuntu0.17`: one new package, no upgrades or +removals. The unchanged REA runtime and target then completed all 13 calls. +The independent earlier bootstrap dialog and controlled package intervention +support the embedding-dependency diagnosis; timing alone does not. + +The shared Linux installer now asks `/usr/bin/python3` for its version and +shared-library location after the original package transaction. It attempts an +actual load. For a missing standard Ubuntu ABI/layout it installs the matching +versioned library and verifies loadability again. APT success alone cannot +establish readiness. Unknown metadata, alternate prefixes and alternate SONAMEs +retain an explicit unknown diagnosis instead of guessing a package. + +Doctor consumes the same fresh, bounded producer, retaining both private-display +and Python observations. Its Python facts concern the system interpreter; +the interpreter actually selected inside Hopper remains unknown. The query +uses isolated Python mode, disables site hooks and bytecode writes, has a +10-second work deadline and 64-KiB output bound, and is not cached: package/interpreter +changes must be visible immediately. The approval plan discloses the conditional +package change without returning the probe program. Existing MCP analysis +contracts and Evidence semantics are unchanged. + +Cancellation prevents starting another install stage and preserves the original +reason. A running package transaction is allowed to settle, avoiding forced +interruption of dpkg. This qualifies the install API's signal behavior through +boundary tests; it does not claim new CLI interrupt plumbing or actual +interruption of a real package transaction. + +A review follow-up preserves cancellation through doctor as well. A cancelled +Python probe now raises the existing domain cancellation error with its original +reason and completed private-display observations; the owned doctor workflow +performs cleanup and attaches any cleanup failure. It no longer becomes an +ordinary unknown diagnostic. The probe also opts into the existing +stop-and-wait supervisor: cancellation and timeout wait for Python to exit, +with the shared five-second grace period and forced-kill fallback. A real +Python fixture holds its SIGTERM handler until the test releases it, checking +that the probe retains ownership until exit. The success-path native measurements below precede +this error-branch correction. Its boundary tests cover both successful cleanup +and cleanup refusal; final-head validation is tracked on the PR. A pre-fix red +execution and a final-head native rerun are not claimed. + +## Pinned environment and artifacts + +Native success measurements pin the initial fix at +`0c70be0030f6bb821c4292c4f5b7f776b365c8ec`, based on main +`23f984b1a884baefc3c44e4ab697f6eff61dffb6`. +The measured runtime and bridge manifest covers 1,733 regular files, 26,537,393 +bytes. Its SHA-256 is +`8800edc8e70965ae2132e57ca6d999ba276503f27f1045b33305c83cd0e2ff2a` +(sorted paths, compact sorted-key JSON records containing path, bytes and +SHA-256). The tiny MCP loader alone is not a whole-runtime identity. +Earlier prototype runs have a separately recorded manifest; they are not +presented as executions of that same artifact. + +- Node 24.18.0; npm 11.16.0; Debian 12 x86-64 host. +- Official [Hopper 6.6.0 Linux demo](https://www.hopperapp.com:443/downloader/public/Hopper-6.6.0-Linux-demo.deb), + 35,748,722 bytes, SHA-256 + `59705e50f7ebf9104abe1e72ac3949bed1a8e43ec67a8c9510a1c5056c200d27`. + REA's real publisher verifier authenticated signing fingerprint + `7C2D73BAFAF5B9A710A69DA1A26DF815099B1BCD`; launcher SHA-256 + `b2271008fb82bf14cfae5c3644cdde2b11fc6804bd7c99d67032ec1b1942d53c`. +- Private [Ubuntu Base 24.04.5 root](https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.5-base-amd64.tar.gz), + SHA-256 `e77b6f10c2590cef872b33ee9f635a0e3fd1f57fb074c0e52b5c7f56147a0c86`, + matched its official HTTPS checksum; its publisher signature was not + independently verified. The host glibc 2.36 cannot run this Hopper package; + the private root supplies glibc 2.39. Related packages were installed only + there. Strict refresh used `APT::Update::Error-Mode=any`. +- Small control ELF: 17,360 bytes, SHA-256 + `1a6f55303712e096ff5196c7fd6e8acf75473fbf0f7467d7b315f96f50b3f841`. + GNU objdump independently maps the arithmetic at `0x401126` to + `arg0 * 7 + 13`; the original build flags were not recovered. The known + `rea_jeb_native_probe` literal has one native string hit. +- Shipped Debian SQLite 3.40.1-2+deb12u2, `libsqlite3.so.0.8.6`, 1,437,848 + bytes, SHA-256 + `2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8`. + Independent objdump identifies `sqlite3_libversion_number` at `0xa1d50`, + returning `0x2e6301` (`3040001`). This is a shipped library qualification, + not a claim about current SQLite versions or whole-program correctness. + +## Actual workflows and measurements + +The accompanying [receipt](hopper-linux-python-runtime.json) preserves native +stage outcomes, elapsed time, CPU time, peak memory, cleanup and token-estimate +scope, including failed and stopped attempts. Installation uses root inside the +owned fixture. Final analysis also uses a normal UID 1004 with a separate home +and the authenticated public publisher evidence copied unchanged into that +user's cache; production verification still checks it. No target was executed +or modified. Non-root package installation through real pkexec authorization, +custom Hopper Python configuration, native Debian installation and other +operating systems remain unverified. + +Fresh final setup reported `ready` / `installed_hopper`, installed the matching +shared library, left `dpkg --audit` empty and passed actual public doctor. +Final normal-user sessions completed the positive, repeated, negative and +decompilation checks: 19 `sqlite_master` matches in 1,437 recognized strings, +74 broader `sqlite` matches and the expected version constant. Both CLI runs +matched their MCP facts and Evidence identity; both target files stayed unchanged. + +| Stage / outcome | Elapsed (s) | Native CPU (s) | Native peak (MiB) | +| ---------------------------------------------- | ----------: | -------------: | ----------------: | +| Baseline setup: ready, library absent | 75.353 | 39.002 | 1395.4 | +| Baseline control MCP: startup timeout | 49.541 | 24.158 | 1282.5 | +| Install only matching libPython | 5.012 | 1.925 | 174.8 | +| Unchanged runtime control MCP: passed | 24.791 | 23.608 | 1228.4 | +| Fixed fresh setup: ready | 80.739 | 40.404 | 1382.6 | +| Fixed public doctor: healthy | 6.035 | 5.160 | 560.9 | +| Fixed normal-user control MCP: 13 calls passed | 24.434 | 23.305 | 1195.7 | +| Fixed normal-user SQLite MCP: 16 calls passed | 92.504 | 90.138 | 1507.2 | +| Fixed normal-user control CLI: parity passed | 8.981 | 7.831 | 613.5 | +| Fixed normal-user SQLite CLI: parity passed | 70.207 | 68.704 | 781.6 | + +Each control MCP session includes discovery and 13 calls: open, status, first +literal query, six repeats, absent query, procedure listing, decompilation and +close. SQLite uses 16 calls: a known exported address avoids a separate name +lookup, and full native string inventory, broader query, regex, case and absence +checks are included. Applicable returned string bytes are checked against the +file. Warm queries must preserve facts and Evidence ID. CLI searches compare +normalized facts and Evidence ID with their corresponding MCP result. Both +target digests are checked after the task. + +“Fresh” means a new installation state or Hopper process, as indicated; OS +caches were not flushed. Installation fixtures preload 247 previously fetched +APT archives (138,023,324 bytes) without installing packages. These are not +cold-network benchmarks. The baseline failure and successful repair are +different outcomes, not a paired speedup. This change adds no analysis cache +and makes no token reduction or complex-investigation advantage claim. + +Native stages have a 6-GiB container cap, 5-GiB early stop, no swap, 128 tasks, +one CPU and explicit affinity. Their separate controller has a 512-MiB cap, +no swap and 64 tasks. Native plus controller caps total 6.5 GiB. Heavy tasks +run serially; admission waits for at least 16 GiB host availability and aborts +below 8 GiB. Host build/check stages have an 8-GiB cap and 7-GiB early stop. +No other user's processes were signalled. + +Native CPU and kernel memory peaks include the container guardian and observer. +Payload-exit observations verify no analysis children remain before stopping +the container. Controller counters are separate partial snapshots; adding +separate peaks is only an upper bound, not a simultaneous measurement. Shared +Docker daemon, containerd and system-manager costs and final counters after +scope removal are unknown. Stopped attempts without payload-exit observations +retain unknown cleanup/counter fields. Observed zero OOM counters at payload +exit are not a claim about the whole host. + +## Failed attempts and limits + +- A single-ID namespace could not apply the real vendor/package UID/GID + ownership changes. It did not qualify a successful installation. +- An inherited container-loopback proxy prevented publisher downloads; the + fixture was corrected to forward the same configured proxy through the + owned bridge gateway. No global proxy configuration was changed. +- An early APT refresh exited zero with failed-index warnings. It was not + counted as a successful refresh; the strict clean run establishes the result. +- One earlier setup was stopped by the host-memory guard during unpacking. + Its interrupted package state was retained separately; clean fixtures were + used for the qualified baseline and final installations. +- SQLite exceeded a caller-selected 30-second startup budget. With the normal + 120-second budget the bridge started, but an initial `libc` positive oracle + failed: those `.dynstr` bytes were not a recognized Hopper string. The + corrected native positive is `sqlite_master`. These attempts remain in the + receipt. Empty recognized-string results do not prove physical absence. +- Early candidate compilation found a TypeScript signal-narrowing error; the + fix reads live signal state through a helper. Lint found oversized new test + groups; they were split by behavior. Failed checks are retained as failures. +- Full local fast checks were stopped twice when other host workloads reduced + available memory. Those attempts did not pass; complete CI and later executed + checks must be reported separately. +- Local package verification completed installation, update, module loading, + discovery and several CLI stages, then the same host-memory guard stopped it + during setup/uninstall after 336.196 seconds (sampled peak 1,742,426,112 + bytes). This is an incomplete package gate, despite the separately completed + real Hopper setup and analysis workflows. Its payload-exit counters and + cleanup observation are unknown. CI must complete this gate before merging. + +Offline `cl100k_base` estimates include each discovered full catalog, named +arguments and raw replies once per recorded session, plus selected CLI arguments +and stdout. Raw MCP replies can contain both text and structured copies. +The final control and SQLite MCP validation transcripts estimate 845,126 and +1,064,574 tokens respectively, including 805,330 catalog tokens each. The CLI +rows estimate 695 and 2,905 tokens for one selected search each. Those CLI rows +cover different tasks from the multi-call MCP qualification sessions, which +also include full-inventory validation; they do not form an interface-efficiency +comparison. Failed attempts and retries have separate rows in the receipt. +Actual agent/model usage, host schema filtering, protocol framing, prompts, +rendering/replay, independent oracle context and unrecorded in-flight failures +are unmeasured. This is provider qualification, not an agent benchmark. diff --git a/src/application/LinuxHopper.test.ts b/src/application/LinuxHopper.test.ts index df2c83aa70..1ba6a1fdb1 100644 --- a/src/application/LinuxHopper.test.ts +++ b/src/application/LinuxHopper.test.ts @@ -28,6 +28,7 @@ import { import type { VerifiedLinuxHopperPackage } from "./LinuxHopperPublisher.js"; import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js"; import * as nativeCommands from "../process/ExecFileOutput.js"; +import * as pythonRuntime from "./LinuxHopperPython.js"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; describe("Linux package-manager command display", () => { @@ -123,6 +124,7 @@ class RecordingLinuxHost implements LinuxHopperInstallHost { installPackage = ( family: LinuxPackageFamily, archive: string, + _options?: { readonly signal?: AbortSignal }, ): Promise => { if (!this.verifiedBeforeInstall) throw new Error("Package manager ran before publisher verification"); @@ -260,6 +262,24 @@ describe("Linux Hopper installation", () => { }); describe("Linux Hopper package installation", () => { + it("keeps runtime inspection failure distinct from package-manager failure and cleans the archive", async () => { + const host = new RecordingLinuxHost(); + const failure = new pythonRuntime.LinuxHopperPythonError( + "configuration_unknown", + "Fixture unknown runtime", + ); + host.installPackage = async () => ({ + status: "runtime_unavailable", + failure, + }); + expect(await installLinuxHopper(host)).toMatchObject({ + status: "failed", + reason: "runtime_dependencies", + pythonRuntimeFailure: failure, + }); + expect(host.cleaned).toEqual(["/tmp/rea test"]); + }); + it.each(["deb", "rpm", "arch"] as const)( "downloads, verifies, installs, reads back, and cleans a %s package", async (family) => { @@ -426,10 +446,30 @@ describe("Linux Hopper cancellation", () => { ).toEqual({ status: "failed", reason: "cancelled", + cancellationReason: controller.signal.reason, }); expect(host.downloads).toEqual([]); expect(host.installed).toEqual([]); }); + + it("forwards cancellation to package stages and retains its original cause through cleanup", async () => { + const host = new RecordingLinuxHost(); + const controller = new AbortController(); + const cause = new Error("caller cancelled between package transactions"); + host.installPackage = async (_family, _archive, options) => { + expect(options?.signal).toBe(controller.signal); + expect(host.verifiedBeforeInstall).toBe(true); + controller.abort(cause); + return { status: "cancelled", cause }; + }; + const result = await installLinuxHopper(host, { + signal: controller.signal, + }); + expect(result).toMatchObject({ status: "failed", reason: "cancelled" }); + if (result.status === "failed") + expect(result.cancellationReason).toBe(cause); + expect(host.cleaned).toEqual(["/tmp/rea test"]); + }); }); describe("Linux Hopper temporary cleanup ownership", () => { diff --git a/src/application/LinuxHopper.ts b/src/application/LinuxHopper.ts index 53bd7a06d1..e5c4ee33f8 100644 --- a/src/application/LinuxHopper.ts +++ b/src/application/LinuxHopper.ts @@ -1,11 +1,16 @@ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { join, posix } from "node:path"; import { execFileBoundedOutput } from "../process/ExecFileBoundedOutput.js"; import { execFileOutput } from "../process/ExecFileOutput.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { downloadHopperPackage } from "./HopperDownload.js"; +import { + inspectLinuxHopperPythonRuntime, + linuxHopperPythonUnavailable, + LinuxHopperPythonError, +} from "./LinuxHopperPython.js"; import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js"; import { err, type Result } from "../domain/result.js"; import { @@ -29,7 +34,7 @@ export type { LinuxPackageFamily } from "./HopperReleaseFeed.js"; /** Package-manager output retained per stream without stopping the command. */ const PACKAGE_MANAGER_OUTPUT_BYTES = 64 * 1024; -/** Exact stable external effects disclosed before a Linux Hopper installation. */ +/** Package mutations disclosed before a Linux Hopper installation. */ export interface LinuxHopperInstallDisclosure { readonly networkOrigins: readonly string[]; readonly integrity: string; @@ -48,11 +53,21 @@ export const linuxHopperInstallDisclosure = ( HOPPER_DOWNLOAD_PREFIX, ], integrity: `Detached PGP signature · primary ${HOPPER_LINUX_PRIMARY_FINGERPRINT} · package byte identity and installed launcher SHA-256 from the official feed`, - commands: linuxPackageManagerCommands( - family, - ``, - isRoot, - ).map(({ executable, args }) => formatCommandForDisplay(executable, args)), + commands: [ + ...linuxPackageManagerCommands( + family, + ``, + isRoot, + ), + ...(family === "deb" + ? [ + debianPythonLibraryCommand( + "", + isRoot, + ), + ] + : []), + ].map(({ executable, args }) => formatCommandForDisplay(executable, args)), }; }; @@ -108,13 +123,21 @@ export interface LinuxPackageInstallFailure { } /** Whether the package-manager step installed the verified package. */ -export type LinuxPackageInstallOutcome = +type LinuxPackageCommandOutcome = | { readonly status: "installed" } | { readonly status: "failed"; readonly failure: LinuxPackageInstallFailure; }; +export type LinuxPackageInstallOutcome = + | LinuxPackageCommandOutcome + | { readonly status: "cancelled"; readonly cause: unknown } + | { + readonly status: "runtime_unavailable"; + readonly failure: LinuxHopperPythonError; + }; + /** External effects required to install the official Linux package. */ export interface LinuxHopperInstallHost { distribution(): Promise; @@ -131,6 +154,7 @@ export interface LinuxHopperInstallHost { installPackage( family: LinuxPackageFamily, archive: string, + options?: { readonly signal?: AbortSignal }, ): Promise; verifyArchive( family: LinuxPackageFamily, @@ -179,6 +203,10 @@ export type LinuxHopperInstallResult = readonly packageFailure?: LinuxPackageInstallFailure; /** Publisher-owned failure, retained for the public setup diagnostic. */ readonly verificationFailure?: HopperReleaseError; + /** System-interpreter inspection, kept separate from package-manager output. */ + readonly pythonRuntimeFailure?: LinuxHopperPythonError; + /** Original caller cancellation, without flattening its value into text. */ + readonly cancellationReason?: unknown; } & { readonly cleanupFailure?: LinuxHopperTemporaryCleanupFailure }); /** Failed removal of an owned temporary install directory. */ @@ -282,8 +310,8 @@ export const installLinuxHopper = async ( readonly signal?: AbortSignal; } = {}, ): Promise => { - if (options.signal?.aborted === true) - return { status: "failed", reason: "cancelled" }; + if (isAborted(options.signal)) + return cancelledInstall(options.signal?.reason); const distribution = await host.distribution(); if (distribution?.hopperSetupSupported !== true) return { status: "failed", reason: "unsupported_host" }; @@ -339,18 +367,27 @@ export const installLinuxHopper = async ( ? "verification_unavailable" : retained.error.reason, }; - if (options.signal?.aborted === true) - return { status: "failed", reason: "cancelled" }; + if (isAborted(options.signal)) + return cancelledInstall(options.signal?.reason); const install = await host.installPackage( distribution.packageFamily, archive, + options, ); + if (install.status === "cancelled") + return cancelledInstall(install.cause); if (install.status === "failed") return { status: "failed", reason: "authorization_or_package_manager", packageFailure: install.failure, }; + if (install.status === "runtime_unavailable") + return { + status: "failed", + reason: "runtime_dependencies", + pythonRuntimeFailure: install.failure, + }; const launcherPath = "/opt/hopper/bin/Hopper"; const launcherStatus = await host.launcherStatus( launcherPath, @@ -367,9 +404,18 @@ export const installLinuxHopper = async ( } catch (cause: unknown) { result = { status: "failed", - reason: isAbortCause(cause) ? "cancelled" : "download", + reason: + isAborted(options.signal) || isAbortCause(cause) + ? "cancelled" + : "download", }; } + if ( + result.status === "failed" && + result.reason === "cancelled" && + isAborted(options.signal) + ) + result = { ...result, cancellationReason: options.signal?.reason }; if (temporary === undefined) return result; try { await host.cleanup(temporary); @@ -418,8 +464,8 @@ export const systemLinuxHopperInstallHost = ( download: downloadHopperPackage, createTemporaryDirectory: () => mkdtemp(join(tmpdir(), "rea-hopper-")), writeArchive: (path, bytes) => writeFile(path, bytes, { mode: 0o600 }), - installPackage: (family, archive) => - installSystemPackage(family, archive, environment), + installPackage: (family, archive, options) => + installSystemPackage(family, archive, environment, options?.signal), verifyArchive: (family, release, archive, signal) => publisher.verifyArchive(family, release, archive, signal), retainVerifiedArchive: (verified, archive, signal) => @@ -503,36 +549,130 @@ const installSystemPackage = async ( family: LinuxPackageFamily, archive: string, environment: Readonly, + signal?: AbortSignal, ): Promise => { - for (const command of linuxPackageManagerCommands( - family, - archive, - process.getuid?.() === 0, - )) { - const output = await execFileBoundedOutput( - command.executable, - command.args, - { - env: environment, - limit: PACKAGE_MANAGER_OUTPUT_BYTES, - }, - ); - if (output.exitCode !== 0) { - return { + const cancelled = (): LinuxPackageInstallOutcome => ({ + status: "cancelled", + cause: signal?.reason, + }); + const isRoot = process.getuid?.() === 0; + for (const command of linuxPackageManagerCommands(family, archive, isRoot)) { + if (isAborted(signal)) return cancelled(); + // Let a running package transaction settle; do not start another after + // the caller cancels. Interrupting dpkg can leave configured state partial. + const result = await runPackageManagerCommand(command, environment); + if (result.status === "failed") return result; + } + if (isAborted(signal)) return cancelled(); + if (family !== "deb") return { status: "installed" }; + + // Ubuntu splits the executable/stdlib from the embedding library. Resolve + // the installed distribution interpreter only after python3 is configured. + const python = await inspectLinuxHopperPythonRuntime(environment, signal); + if (isAborted(signal)) return cancelled(); + if (!python.ok) return pythonInspectionFailure(python.error); + if (python.value.library.status === "loadable") + return { status: "installed" }; + if (python.value.library.status === "unknown") + return { + status: "runtime_unavailable", + failure: linuxHopperPythonUnavailable(python.value.library), + }; + const packageName = "libpython" + python.value.version.join("."); + const libraryPath = python.value.library.path; + // Only the standard Ubuntu shared ABI/layout establishes this APT mapping. + // Preserve alternate prefixes/SONAMEs as unknown instead of guessing a package. + if ( + posix.basename(libraryPath) !== packageName + ".so.1.0" || + !/^\/usr\/lib\/[a-z0-9_]+-linux-gnu[a-z0-9_]*$/u.test( + posix.dirname(libraryPath), + ) + ) + return { + status: "runtime_unavailable", + failure: new LinuxHopperPythonError( + "configuration_unknown", + "Cannot determine the matching APT runtime package for " + + libraryPath + + ". Repair this interpreter's shared runtime before retrying.", + ), + }; + if (isAborted(signal)) return cancelled(); + const installed = await runPackageManagerCommand( + debianPythonLibraryCommand(packageName, isRoot), + environment, + ); + if (installed.status === "failed") return installed; + if (isAborted(signal)) return cancelled(); + // A successful APT exit is not proof that the selected runtime can load. + const verified = await inspectLinuxHopperPythonRuntime(environment, signal); + if (isAborted(signal)) return cancelled(); + if (!verified.ok) return pythonInspectionFailure(verified.error); + if (verified.value.library.status !== "loadable") + return { + status: "runtime_unavailable", + failure: linuxHopperPythonUnavailable(verified.value.library), + }; + return { status: "installed" }; +}; + +const isAborted = (signal: AbortSignal | undefined): boolean => + signal?.aborted === true; + +const cancelledInstall = (cause: unknown): LinuxHopperInstallResult => ({ + status: "failed", + reason: "cancelled", + cancellationReason: cause, +}); + +const pythonInspectionFailure = ( + failure: LinuxHopperPythonError, +): LinuxPackageInstallOutcome => + failure.reason === "cancelled" + ? { status: "cancelled", cause: failure.cause } + : { status: "runtime_unavailable", failure }; + +interface PackageCommand { + readonly executable: string; + readonly args: readonly string[]; +} + +const withPackagePrivilege = ( + executable: string, + args: readonly string[], + isRoot: boolean, +): PackageCommand => + isRoot + ? { executable, args } + : { executable: "pkexec", args: [executable, ...args] }; + +const debianPythonLibraryCommand = ( + packageName: string, + isRoot: boolean, +): PackageCommand => + withPackagePrivilege("apt-get", ["install", "-y", packageName], isRoot); + +const runPackageManagerCommand = async ( + command: PackageCommand, + environment: Readonly, +): Promise => { + const output = await execFileBoundedOutput(command.executable, command.args, { + env: environment, + limit: PACKAGE_MANAGER_OUTPUT_BYTES, + }); + return output.exitCode === 0 + ? { status: "installed" } + : { status: "failed", failure: { command: formatCommandForDisplay(command.executable, command.args), exitCode: output.exitCode, - // The streams stay separate; their original interleaved order - // is not recoverable after capture. + // Preserve the separately captured streams and truncation marker. stdout: output.stdout, stderr: output.stderr, outputTruncated: output.outputTruncated, }, }; - } - } - return { status: "installed" }; }; /** Preserve argument boundaries in a command shown to the user. */ @@ -551,10 +691,7 @@ export const linuxPackageManagerCommands = ( family: LinuxPackageFamily, archive: string, isRoot: boolean, -): readonly { - readonly executable: string; - readonly args: readonly string[]; -}[] => { +): readonly PackageCommand[] => { const runtimeDependencies = family === "deb" ? ["xvfb", "xauth", "python3", "libx11-6", "libxtst6"] @@ -577,9 +714,9 @@ export const linuxPackageManagerCommands = ( ["pacman", "-U", "--noconfirm", archive], ]; return commands.map((command) => { - const executable = isRoot ? command[0] : "pkexec"; + const executable = command[0]; if (executable === undefined) throw new Error("package command is empty"); - return { executable, args: isRoot ? command.slice(1) : command }; + return withPackagePrivilege(executable, command.slice(1), isRoot); }); }; diff --git a/src/application/LinuxHopperPackageRuntime.test.ts b/src/application/LinuxHopperPackageRuntime.test.ts new file mode 100644 index 0000000000..1acd97b6bd --- /dev/null +++ b/src/application/LinuxHopperPackageRuntime.test.ts @@ -0,0 +1,264 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; +import * as boundedCommands from "../process/ExecFileBoundedOutput.js"; +import { err, ok } from "../domain/result.js"; +import * as pythonRuntime from "./LinuxHopperPython.js"; +import { + linuxHopperInstallDisclosure, + systemLinuxHopperInstallHost, +} from "./LinuxHopper.js"; + +const missingPython = { + interpreter: "/usr/bin/python3", + version: [3, 14], + library: { + status: "unavailable", + path: "/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0", + reason: "No such file", + }, +} satisfies pythonRuntime.LinuxHopperPythonRuntime; +const readyPython = { + ...missingPython, + library: { + status: "loadable", + path: "/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0", + }, +} satisfies pythonRuntime.LinuxHopperPythonRuntime; +const commandSuccess = { + exitCode: 0, + stdout: "", + stderr: "", + outputTruncated: false, +}; + +describe("Linux Hopper package runtime", () => { + it.each([ + { + name: "installs and verifies the matching future Python library", + inspections: [ok(missingPython), ok(readyPython)], + packageCalls: 2, + status: "installed", + }, + { + name: "skips the extra APT command for an already loadable library", + inspections: [ok(readyPython)], + packageCalls: 1, + status: "installed", + }, + { + name: "does not guess a package for an unknown library location", + inspections: [ + ok({ + ...missingPython, + library: { status: "unknown", reason: "No INSTSONAME" }, + } satisfies pythonRuntime.LinuxHopperPythonRuntime), + ], + packageCalls: 1, + status: "runtime_unavailable", + }, + { + name: "rejects APT success when the library still cannot load", + inspections: [ok(missingPython), ok(missingPython)], + packageCalls: 2, + status: "runtime_unavailable", + }, + { + name: "preserves an interpreter inspection failure", + inspections: [ + err( + new pythonRuntime.LinuxHopperPythonError( + "query_failed", + "Fixture interpreter unavailable", + ), + ), + ], + packageCalls: 1, + status: "runtime_unavailable", + }, + ])("$name", async ({ inspections, packageCalls, status }) => { + const directory = await createTestTempDirectory( + "rea-linux-python-install-", + ); + const host = systemLinuxHopperInstallHost({ XDG_CACHE_HOME: directory }); + const command = vi + .spyOn(boundedCommands, "execFileBoundedOutput") + .mockResolvedValue(commandSuccess); + const inspect = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockRejectedValue(new Error("Unexpected extra Python inspection")); + for (const result of inspections) inspect.mockResolvedValueOnce(result); + try { + expect( + await host.installPackage("deb", "/tmp/verified-hopper.deb"), + ).toMatchObject({ status }); + expect(command).toHaveBeenCalledTimes(packageCalls); + expect(inspect).toHaveBeenCalledTimes(inspections.length); + if (packageCalls === 2) { + const isRoot = process.getuid?.() === 0; + expect(command.mock.calls[1]?.slice(0, 2)).toEqual([ + isRoot ? "apt-get" : "pkexec", + [...(isRoot ? [] : ["apt-get"]), "install", "-y", "libpython3.14"], + ]); + } + } finally { + try { + await host.close?.(); + } finally { + inspect.mockRestore(); + command.mockRestore(); + } + } + }); + + it("preserves a failed runtime APT command and does not verify it as installed", async () => { + const directory = await createTestTempDirectory( + "rea-linux-python-failure-", + ); + const host = systemLinuxHopperInstallHost({ XDG_CACHE_HOME: directory }); + const failed = { + exitCode: 67, + stdout: "original package output", + stderr: "original package error", + outputTruncated: true, + }; + const command = vi + .spyOn(boundedCommands, "execFileBoundedOutput") + .mockResolvedValueOnce(commandSuccess) + .mockResolvedValueOnce(failed); + const inspect = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockResolvedValue(ok(missingPython)); + try { + expect( + await host.installPackage("deb", "/tmp/verified-hopper.deb"), + ).toMatchObject({ status: "failed", failure: failed }); + expect(command).toHaveBeenCalledTimes(2); + expect(inspect).toHaveBeenCalledTimes(1); + } finally { + try { + await host.close?.(); + } finally { + inspect.mockRestore(); + command.mockRestore(); + } + } + }); +}); + +describe("Linux Hopper package mapping", () => { + it.each([ + "/custom/libpython3.14.so.1.0", + "/usr/lib/x86_64-linux-gnu/libpython3.14d.so.1.0", + "/usr/lib/x86_64-linux-gnu/libpython3.13.so.1.0", + ])("keeps an unproven APT mapping unknown: %s", async (path) => { + const directory = await createTestTempDirectory("rea-python-package-map-"); + const host = systemLinuxHopperInstallHost({ XDG_CACHE_HOME: directory }); + const command = vi + .spyOn(boundedCommands, "execFileBoundedOutput") + .mockResolvedValue(commandSuccess); + const inspect = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockResolvedValue( + ok({ + ...missingPython, + library: { ...missingPython.library, path }, + }), + ); + try { + expect( + await host.installPackage("deb", "/tmp/verified-hopper.deb"), + ).toMatchObject({ + status: "runtime_unavailable", + failure: { + reason: "configuration_unknown", + message: expect.stringContaining(path), + }, + }); + expect(command).toHaveBeenCalledTimes(1); + expect(inspect).toHaveBeenCalledTimes(1); + } finally { + try { + await host.close?.(); + } finally { + inspect.mockRestore(); + command.mockRestore(); + } + } + }); +}); + +describe("Linux Hopper package cancellation and disclosure", () => { + it.each([ + { phase: "before", commands: 0, inspections: 0 }, + { phase: "first transaction", commands: 1, inspections: 0 }, + { phase: "query", commands: 1, inspections: 1 }, + { phase: "runtime transaction", commands: 2, inspections: 1 }, + { phase: "verification", commands: 2, inspections: 2 }, + ])( + "honors cancellation at $phase without starting the next stage", + async ({ phase, commands, inspections }) => { + const directory = await createTestTempDirectory( + "rea-python-install-cancel-", + ); + const host = systemLinuxHopperInstallHost({ XDG_CACHE_HOME: directory }); + const controller = new AbortController(); + const cause = new Error("caller cancelled at " + phase); + let commandCount = 0; + let inspectionCount = 0; + const command = vi + .spyOn(boundedCommands, "execFileBoundedOutput") + .mockImplementation(async () => { + commandCount++; + if ( + (phase === "first transaction" && commandCount === 1) || + (phase === "runtime transaction" && commandCount === 2) + ) + controller.abort(cause); + return commandSuccess; + }); + const inspect = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockImplementation(async (_environment, signal) => { + expect(signal).toBe(controller.signal); + inspectionCount++; + if ( + phase === "query" || + (phase === "verification" && inspectionCount === 2) + ) + controller.abort(cause); + return ok(inspectionCount === 1 ? missingPython : readyPython); + }); + if (phase === "before") controller.abort(cause); + try { + const result = await host.installPackage( + "deb", + "/tmp/verified-hopper.deb", + { + signal: controller.signal, + }, + ); + expect(result).toEqual({ status: "cancelled", cause }); + if (result.status === "cancelled") expect(result.cause).toBe(cause); + expect(command).toHaveBeenCalledTimes(commands); + expect(inspect).toHaveBeenCalledTimes(inspections); + } finally { + try { + await host.close?.(); + } finally { + inspect.mockRestore(); + command.mockRestore(); + } + } + }, + ); + + it("discloses the conditional privileged library install without returning probe code", () => { + const disclosure = linuxHopperInstallDisclosure("deb", false); + expect(disclosure.commands).toHaveLength(2); + expect(disclosure.commands[1]).toBe( + 'pkexec apt-get install -y ""', + ); + expect(linuxHopperInstallDisclosure("rpm", false).commands).toHaveLength(1); + }); +}); diff --git a/src/application/LinuxHopperPython.test.ts b/src/application/LinuxHopperPython.test.ts new file mode 100644 index 0000000000..10b365136d --- /dev/null +++ b/src/application/LinuxHopperPython.test.ts @@ -0,0 +1,257 @@ +import { existsSync } from "node:fs"; +import { access, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { describe, expect, it, vi } from "vitest"; + +import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; +import { execFileOutput } from "../process/ExecFileOutput.js"; +import { inspectLinuxHopperPythonRuntime } from "./LinuxHopperPython.js"; + +const response = (value: unknown) => { + const stdout = JSON.stringify(value); + return { + stdout, + stderr: "", + stdoutBytes: Buffer.byteLength(stdout), + stderrBytes: 0, + }; +}; +const available = { + version: [3, 14], + library: { status: "loadable", path: "/usr/lib/libpython3.14.so.1.0" }, +}; + +describe("distribution Python runtime inspection", () => { + it("preserves version and library identity without assuming Python 3.12", async () => { + const execute = vi + .fn() + .mockResolvedValue(response(available)); + const environment = { + PYTHONHOME: "/caller/python", + PYTHONPATH: "/caller/modules", + }; + const result = await inspectLinuxHopperPythonRuntime( + environment, + undefined, + execute, + ); + expect(result).toEqual({ + ok: true, + value: { ...available, interpreter: "/usr/bin/python3" }, + }); + expect(execute).toHaveBeenCalledWith( + "/usr/bin/python3", + expect.arrayContaining(["-I", "-S", "-B"]), + expect.objectContaining({ + env: environment, + timeout: 10_000, + maxBuffer: 64 * 1024, + stopSignal: "SIGTERM", + }), + ); + }); + + it.each([ + { version: [2, 7], library: available.library }, + { version: [3, "14"], library: available.library }, + { + version: [3, 14], + library: { status: "loadable", path: "relative/libpython.so" }, + }, + { + version: [3, 14], + library: { status: "loadable", path: "C:\\not-a-linux-path\\python.dll" }, + }, + { + version: [3, 14], + library: { status: "loadable", path: "/usr/lib/python\0.so" }, + }, + { + version: [3, 14], + library: { status: "unavailable", path: "/lib/python.so" }, + }, + null, + ])( + "refuses malformed facts without reporting library absence: %j", + async (value) => { + const execute = vi + .fn() + .mockResolvedValue(response(value)); + const result = await inspectLinuxHopperPythonRuntime( + {}, + undefined, + execute, + ); + expect(result).toMatchObject({ + ok: false, + error: { reason: "invalid_response" }, + }); + }, + ); + + it("retains unknown configuration and observes a later repaired library afresh", async () => { + const unknown = { + version: [3, 14], + library: { status: "unknown", reason: "No INSTSONAME" }, + }; + const execute = vi + .fn() + .mockResolvedValueOnce(response(unknown)) + .mockResolvedValueOnce(response(available)); + expect( + await inspectLinuxHopperPythonRuntime({}, undefined, execute), + ).toMatchObject({ ok: true, value: unknown }); + expect( + await inspectLinuxHopperPythonRuntime({}, undefined, execute), + ).toMatchObject({ ok: true, value: available }); + expect(execute).toHaveBeenCalledTimes(2); + }); + + it("distinguishes execution and JSON failures from an unavailable library", async () => { + const execute = vi + .fn() + .mockRejectedValueOnce(new Error("fixture execution failure")) + .mockResolvedValueOnce({ + stdout: "{", + stderr: "", + stdoutBytes: 1, + stderrBytes: 0, + }); + expect( + await inspectLinuxHopperPythonRuntime({}, undefined, execute), + ).toMatchObject({ ok: false, error: { reason: "query_failed" } }); + expect( + await inspectLinuxHopperPythonRuntime({}, undefined, execute), + ).toMatchObject({ ok: false, error: { reason: "invalid_response" } }); + }); +}); + +describe("distribution Python process cleanup", () => { + it.skipIf(process.platform !== "linux" || !existsSync("/usr/bin/python3"))( + "waits for a delayed Python exit before completing cancellation", + async () => { + const directory = await createTestTempDirectory("rea-python-stop-"); + const ready = join(directory, "ready"); + const stopping = join(directory, "stopping"); + const release = join(directory, "release"); + const stopped = join(directory, "stopped"); + const program = `from pathlib import Path +import signal, sys, time +def stop(signum, frame): + Path(${JSON.stringify(stopping)}).write_text(str(signum)) + while not Path(${JSON.stringify(release)}).exists(): + time.sleep(0.01) + Path(${JSON.stringify(stopped)}).write_text(str(signum)) + sys.exit(0) +signal.signal(signal.SIGTERM, stop) +Path(${JSON.stringify(ready)}).write_text("ready") +while True: + time.sleep(0.1) +`; + const controller = new AbortController(); + const reason = { request: "stop owned Python" }; + let settled = false; + const execution = inspectLinuxHopperPythonRuntime( + process.env, + controller.signal, + (command, _arguments, options) => + execFileOutput(command, ["-I", "-S", "-B", "-c", program], options), + ).then((result) => { + settled = true; + return result; + }); + try { + await vi.waitFor(() => access(ready), { timeout: 5_000, interval: 10 }); + controller.abort(reason); + await vi.waitFor(() => access(stopping), { + timeout: 3_000, + interval: 10, + }); + // The child cannot exit until released; early settlement loses ownership. + expect(settled).toBe(false); + } finally { + try { + await writeFile(release, "release"); + } finally { + controller.abort(reason); + await execution; + } + await vi.waitFor(() => access(stopped), { + timeout: 5_000, + interval: 10, + }); + } + expect(await readFile(stopped, "utf8")).toBe("15"); + const result = await execution; + expect(result).toMatchObject({ + ok: false, + error: { reason: "cancelled" }, + }); + if (!result.ok) expect(result.error.cause).toBe(reason); + }, + ); +}); + +describe("distribution Python isolation and cancellation", () => { + it("preserves cancellation and starts no interpreter for an already cancelled query", async () => { + const controller = new AbortController(); + const reason = new Error("caller cancellation"); + controller.abort(reason); + const execute = vi.fn(); + const result = await inspectLinuxHopperPythonRuntime( + {}, + controller.signal, + execute, + ); + expect(result).toMatchObject({ + ok: false, + error: { reason: "cancelled", cause: reason }, + }); + expect(execute).not.toHaveBeenCalled(); + }); + + it("observes cancellation that arrives during interpreter execution", async () => { + const controller = new AbortController(); + const reason = new Error("cancelled during query"); + const execute = vi + .fn() + .mockImplementation(async () => { + controller.abort(reason); + return response(available); + }); + const result = await inspectLinuxHopperPythonRuntime( + {}, + controller.signal, + execute, + ); + expect(result).toMatchObject({ + ok: false, + error: { reason: "cancelled", cause: reason }, + }); + }); + + it.skipIf(process.platform !== "linux" || !existsSync("/usr/bin/python3"))( + "runs the real producer without loading caller Python modules or PYTHONHOME", + async () => { + const directory = await createTestTempDirectory("rea-system-python-"); + for (const name of ["ctypes.py", "sysconfig.py", "sitecustomize.py"]) + await writeFile( + join(directory, name), + "raise RuntimeError('caller module was loaded')\n", + ); + const result = await inspectLinuxHopperPythonRuntime({ + ...process.env, + PYTHONPATH: directory, + PYTHONHOME: join(directory, "missing-python-home"), + }); + expect(result).toMatchObject({ + ok: true, + value: { + interpreter: "/usr/bin/python3", + version: [3, expect.any(Number)], + }, + }); + }, + ); +}); diff --git a/src/application/LinuxHopperPython.ts b/src/application/LinuxHopperPython.ts new file mode 100644 index 0000000000..e586ca2cbc --- /dev/null +++ b/src/application/LinuxHopperPython.ts @@ -0,0 +1,157 @@ +import { posix } from "node:path"; + +import { z } from "zod"; + +import { err, ok, type Result } from "../domain/result.js"; +import { execFileOutput } from "../process/ExecFileOutput.js"; + +const libraryPath = z + .string() + .refine((value) => posix.isAbsolute(value) && !value.includes("\0")); +const runtimeSchema = z.object({ + version: z.tuple([z.literal(3), z.number().int().nonnegative()]), + library: z.discriminatedUnion("status", [ + z.object({ status: z.literal("loadable"), path: libraryPath }), + z.object({ + status: z.literal("unavailable"), + path: libraryPath, + reason: z.string().min(1), + }), + z.object({ status: z.literal("unknown"), reason: z.string().min(1) }), + ]), +}); + +/** Facts reported by the isolated system interpreter, without a GUI launch. */ +export type LinuxHopperPythonRuntime = z.infer & { + readonly interpreter: "/usr/bin/python3"; +}; + +/** A failed inspection does not establish that a shared library is absent. */ +export class LinuxHopperPythonError extends Error { + readonly _tag = "LinuxHopperPythonError"; + + constructor( + readonly reason: + | "query_failed" + | "invalid_response" + | "configuration_unknown" + | "runtime_unavailable" + | "cancelled", + message: string, + cause?: unknown, + ) { + super(message, { cause }); + } +} + +// Isolate caller Python paths/site hooks and disable bytecode writes. Inspect +// the distribution interpreter that the package manager installs, not PATH. +const query = `import ctypes, json, os, sys, sysconfig +directory = sysconfig.get_config_var("LIBDIR") +name = sysconfig.get_config_var("INSTSONAME") +if not isinstance(directory, str) or not os.path.isabs(directory) or not isinstance(name, str) or not name: + library = {"status": "unknown", "reason": "Python did not report an absolute shared-library location"} +else: + path = os.path.join(directory, name) + try: + ctypes.CDLL(path) + library = {"status": "loadable", "path": path} + except OSError as error: + library = {"status": "unavailable", "path": path, "reason": str(error)} +print(json.dumps({"version": list(sys.version_info[:2]), "library": library})) +`; + +const runtimeCommand = { + executable: "/usr/bin/python3", + args: ["-I", "-S", "-B", "-c", query], +} as const; + +// AbortSignal changes while an awaited process runs, despite its readonly type. +const isAborted = (signal: AbortSignal | undefined): boolean => + signal?.aborted === true; + +/** Fresh, bounded inspection; package or interpreter changes must be observable. */ +export const inspectLinuxHopperPythonRuntime = async ( + environment: Readonly, + signal?: AbortSignal, + execute: typeof execFileOutput = execFileOutput, +): Promise> => { + const cancelled = () => + err( + new LinuxHopperPythonError( + "cancelled", + "Python runtime inspection was cancelled.", + signal?.reason, + ), + ); + if (isAborted(signal)) return cancelled(); + let output: string; + try { + const result = await execute( + runtimeCommand.executable, + runtimeCommand.args, + { + env: environment, + signal, + stopSignal: "SIGTERM", + timeout: 10_000, + maxBuffer: 64 * 1024, + }, + ); + output = result.stdout; + } catch (cause: unknown) { + if (isAborted(signal)) return cancelled(); + return err( + new LinuxHopperPythonError( + "query_failed", + "Could not inspect the shared runtime of /usr/bin/python3.", + cause, + ), + ); + } + if (isAborted(signal)) return cancelled(); + let value: unknown; + try { + value = JSON.parse(output); + } catch (cause: unknown) { + return err( + new LinuxHopperPythonError( + "invalid_response", + "System Python returned an invalid shared-runtime description.", + cause, + ), + ); + } + const parsed = runtimeSchema.safeParse(value); + if (!parsed.success) + return err( + new LinuxHopperPythonError( + "invalid_response", + "System Python returned an invalid shared-runtime description.", + parsed.error, + ), + ); + return ok({ + ...parsed.data, + interpreter: runtimeCommand.executable, + }); +}; + +/** Explain a known failed load separately from an unknown configuration. */ +export const linuxHopperPythonUnavailable = ( + library: Exclude< + LinuxHopperPythonRuntime["library"], + { readonly status: "loadable" } + >, +): LinuxHopperPythonError => + new LinuxHopperPythonError( + library.status === "unknown" + ? "configuration_unknown" + : "runtime_unavailable", + library.status === "unavailable" + ? "The system Python shared runtime cannot load " + + library.path + + ": " + + library.reason + : "System Python did not identify its shared runtime: " + library.reason, + ); diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts index 8eb27d8df9..8317b20f9b 100644 --- a/src/application/SetupHost.ts +++ b/src/application/SetupHost.ts @@ -222,10 +222,13 @@ export const systemSetupHost = ( result.packageFailure, "linux", ); - return result.verificationFailure !== undefined + const detail = + result.verificationFailure?.message ?? + result.pythonRuntimeFailure?.message; + return detail !== undefined ? { ...failure, - remediation: `${result.verificationFailure.message} ${failure.remediation}`, + remediation: `${detail} ${failure.remediation}`, } : failure; } diff --git a/src/application/SetupInstallFailure.ts b/src/application/SetupInstallFailure.ts index 6bb825054f..0df697b188 100644 --- a/src/application/SetupInstallFailure.ts +++ b/src/application/SetupInstallFailure.ts @@ -136,7 +136,7 @@ export const setupInstallFailure = ( status: "failed", code: "runtime_dependency_unavailable", remediation: - "Hopper was installed but required shared libraries are missing. Rerun rea setup, then apply the hopper-demo-runtime remediation from rea doctor.", + "Hopper's required runtime could not be verified. Inspect rea doctor --provider hopper; on a supported system use rea setup --install-hopper to repair its dependencies.", }; case "unsupported_hopper_build": return { @@ -150,7 +150,7 @@ export const setupInstallFailure = ( status: "failed", code: "setup_cancelled", remediation: - "Setup was cancelled before Hopper was installed. Rerun rea setup when ready.", + "Setup was cancelled. Run rea doctor to inspect the current state before retrying.", }; case "destination_exists": return { diff --git a/src/application/SetupPlan.ts b/src/application/SetupPlan.ts index 3a7b3d0f1d..e43f3877ab 100644 --- a/src/application/SetupPlan.ts +++ b/src/application/SetupPlan.ts @@ -301,7 +301,7 @@ const setupPlan = (input: { : "system package manager", detail: input.platform === "linux" - ? `Download the current official Hopper demo, verify its PGP publisher and launcher bytes, and retain archive, public-key.asc and release.json under ${input.hopperCacheDirectory ?? `${input.homeDirectory}/.cache/rea/hopper-releases`}/ for offline verification. Preserve replaced damaged evidence beside it as .backup-. Install Hopper plus its Xvfb demo-session dependencies. REA uses a private display and selects Hopper's offered demo mode for each analysis session.` + ? `Download the current official Hopper demo, verify its PGP publisher and launcher bytes, and retain archive, public-key.asc and release.json under ${input.hopperCacheDirectory ?? `${input.homeDirectory}/.cache/rea/hopper-releases`}/ for offline verification. Preserve replaced damaged evidence beside it as .backup-. Install Hopper plus its Xvfb demo-session dependencies${input.linuxPackageFamily === "deb" ? ", including the shared library matching the distribution Python interpreter" : ""}. REA uses a private display and selects Hopper's offered demo mode for each analysis session.` : "Download the official Hopper package, verify it, and install it. Hopper may show its demo or license prompt when first opened.", external: true, operation: "install" as const, diff --git a/src/doctorRuntime.test.ts b/src/doctorRuntime.test.ts index 265c96c5e1..ce33ccca9b 100644 --- a/src/doctorRuntime.test.ts +++ b/src/doctorRuntime.test.ts @@ -1,13 +1,202 @@ import { chmod, mkdir, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { expect, it as test } from "vitest"; +import { expect, it as test, vi } from "vitest"; import { createSystemDoctorHost } from "./doctorRuntime.js"; import { DOCTOR_PROVIDER_IDS } from "./application/DoctorProviderIds.js"; import { providerRegistrationEnvironment } from "./application/SetupRegistrationEnvironment.js"; import { createTestTempDirectory } from "../tests/fixtures/temporaryDirectory.js"; import { writeJadxJarInventory } from "../tests/fixtures/android/jadxJar.js"; +import * as privateDisplay from "./hopper/LinuxPrivateDisplayProbe.js"; +import * as pythonRuntime from "./application/LinuxHopperPython.js"; +import { err, ok } from "./domain/result.js"; +import { AnalysisError } from "./domain/analysisErrorBase.js"; +import { AnalysisCancelledError } from "./domain/analysisErrorCore.js"; +import { createDoctorHostFixture } from "./application/Doctor.fixture.js"; +import { runDoctorOwned } from "./application/Doctor.js"; + +const readyDisplay = { + ok: true, + strategy: "direct", + diagnostic: { + component: "hopper_private_display", + operation: "probe", + reason: "ready", + socket_directory: "/tmp/.X11-unix", + socket_directory_mode: "1777", + mount_read_only: false, + effective_socket_directory_mode: "1777", + effective_mount_read_only: false, + wsl: false, + strategy: "direct", + fallback_reason: null, + xvfb_stderr_bytes: 0, + status: "ready", + failure_code: null, + }, +} as const satisfies privateDisplay.LinuxPrivateDisplaySelection; + +test.each([undefined, "controlled close refusal"])( + "propagates runtime-probe cancellation through owned doctor cleanup: %s", + async (cleanupFailure) => { + const controller = new AbortController(); + const reason = { request: "cancel Python runtime inspection" }; + const display = vi + .spyOn(privateDisplay, "selectLinuxPrivateDisplayStrategy") + .mockResolvedValue(readyDisplay); + const python = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockImplementation(async (_environment, signal) => { + expect(signal).toBe(controller.signal); + controller.abort(reason); + return err( + new pythonRuntime.LinuxHopperPythonError( + "cancelled", + "Controlled runtime cancellation", + reason, + ), + ); + }); + const systemHost = createSystemDoctorHost({}, controller.signal); + const close = vi.fn(async () => { + await systemHost.close?.(); + return cleanupFailure; + }); + try { + const failure = await runDoctorOwned( + undefined, + createDoctorHostFixture({ + platform: "linux", + configuredHopperPath: "/fixture/Hopper", + signal: controller.signal, + linuxDemoRuntimeCheck: systemHost.linuxDemoRuntimeCheck, + close, + }), + ).then( + () => undefined, + (cause: unknown) => cause, + ); + if (!(failure instanceof AnalysisError)) + throw new Error("Expected a doctor cancellation failure"); + const primary = failure.cleanupPrimary ?? failure; + expect(primary).toBeInstanceOf(AnalysisCancelledError); + expect(primary.cause).toBe(reason); + expect(primary.diagnostics).toMatchObject({ + hopper_demo_runtime: { + private_display: readyDisplay.diagnostic, + embedded_python: { status: "unknown", reason: "cancelled" }, + }, + }); + expect(close).toHaveBeenCalledTimes(1); + if (cleanupFailure === undefined) expect(failure).toBe(primary); + else expect(failure.cleanupIncomplete).toBe(true); + } finally { + try { + await systemHost.close?.(); + } finally { + python.mockRestore(); + display.mockRestore(); + } + } + }, +); + +test.each(["unavailable", "unknown"] as const)( + "does not report a healthy Hopper runtime when Python is %s", + async (state) => { + const display = vi + .spyOn(privateDisplay, "selectLinuxPrivateDisplayStrategy") + .mockResolvedValue(readyDisplay); + const python = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockResolvedValue( + state === "unavailable" + ? ok({ + interpreter: "/usr/bin/python3", + version: [3, 14], + library: { + status: "unavailable", + path: "/usr/lib/libpython3.14.so.1.0", + reason: "No such file", + }, + }) + : err( + new pythonRuntime.LinuxHopperPythonError( + "query_failed", + "Fixture query failed", + ), + ), + ); + try { + const check = await createSystemDoctorHost({}).linuxDemoRuntimeCheck?.(); + expect(check).toMatchObject({ + name: "hopper-demo-runtime", + ok: false, + classification: + state === "unavailable" ? "missing_dependency" : "unknown", + details: { + private_display: readyDisplay.diagnostic, + embedded_python: { + scope: "system_interpreter", + hopper_interpreter: "unknown", + ...(state === "unavailable" + ? { status: "observed", library: { status: "unavailable" } } + : { status: "unknown", reason: "query_failed" }), + }, + }, + }); + expect(check?.remediation).toContain( + state === "unavailable" + ? "rea setup --install-hopper" + : "Retry rea doctor", + ); + } finally { + python.mockRestore(); + display.mockRestore(); + } + }, +); + +test("preserves both X11 and Python failures and their independent remediation", async () => { + const display = vi + .spyOn(privateDisplay, "selectLinuxPrivateDisplayStrategy") + .mockResolvedValue({ + ok: false, + strategy: "unavailable", + exitCode: 80, + diagnostic: { + ...readyDisplay.diagnostic, + status: "error", + failure_code: "x11_socket_directory_unusable", + }, + }); + const python = vi + .spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime") + .mockResolvedValue( + ok({ + interpreter: "/usr/bin/python3", + version: [3, 14], + library: { status: "unknown", reason: "No INSTSONAME" }, + }), + ); + try { + const check = await createSystemDoctorHost({}).linuxDemoRuntimeCheck?.(); + expect(check).toMatchObject({ + ok: false, + classification: "config_drift", + details: { + private_display: { failure_code: "x11_socket_directory_unusable" }, + embedded_python: { status: "observed", library: { status: "unknown" } }, + }, + }); + expect(check?.remediation).toContain("user and mount namespaces"); + expect(check?.remediation).toContain("shared-runtime configuration"); + } finally { + python.mockRestore(); + display.mockRestore(); + } +}); const it = test.skipIf(process.platform === "win32"); diff --git a/src/doctorRuntime.ts b/src/doctorRuntime.ts index 78a34beb71..2b3c1e1c08 100644 --- a/src/doctorRuntime.ts +++ b/src/doctorRuntime.ts @@ -12,6 +12,11 @@ import { readIdaConfiguration } from "./ida/IdaConfiguration.js"; import { parseJebMcpEndpoint } from "./jeb/JebMcpEndpoint.js"; import { selectLinuxPrivateDisplayStrategy } from "./hopper/LinuxPrivateDisplayProbe.js"; import { ok } from "./domain/result.js"; +import { AnalysisCancelledError } from "./domain/analysisErrorCore.js"; +import { + inspectLinuxHopperPythonRuntime, + linuxHopperPythonUnavailable, +} from "./application/LinuxHopperPython.js"; /** Compose provider diagnostics at the outer CLI adapter boundary. */ export const createSystemDoctorHost = ( @@ -60,7 +65,8 @@ export const createSystemDoctorHost = ( fileCloseResources, }), ], - linuxDemoRuntimeCheck: () => inspectLinuxPrivateDisplay(environment), + linuxDemoRuntimeCheck: () => + inspectLinuxHopperDemoRuntime(environment, signal), }); }; @@ -68,13 +74,31 @@ const HOPPER_DEMO_HELPER_PATH = fileURLToPath( new URL("../scripts/hopper-demo-x11.py", import.meta.url), ); -const inspectLinuxPrivateDisplay = async ( +const inspectLinuxHopperDemoRuntime = async ( environment: Readonly, + signal?: AbortSignal, ): Promise => { const selection = await selectLinuxPrivateDisplayStrategy({ helperPath: HOPPER_DEMO_HELPER_PATH, environment, }); + const python = await inspectLinuxHopperPythonRuntime(environment, signal); + const pythonFailure = !python.ok + ? python.error + : python.value.library.status !== "loadable" + ? linuxHopperPythonUnavailable(python.value.library) + : undefined; + const pythonDiagnostic = { + scope: "system_interpreter", + hopper_interpreter: "unknown", + ...(python.ok + ? { status: "observed", ...python.value } + : { + status: "unknown", + reason: python.error.reason, + message: python.error.message, + }), + }; const value = selection.diagnostic; const detail = [ `strategy=${selection.strategy}`, @@ -84,29 +108,60 @@ const inspectLinuxPrivateDisplay = async ( `effective_mode=${value.effective_socket_directory_mode ?? "unknown"}`, `effective_mount_read_only=${String(value.effective_mount_read_only)}`, `wsl=${String(value.wsl)}`, + `system_python=${python.ok ? python.value.library.status : "unknown"}`, ].join("; "); - if (selection.ok) + const details = { + private_display: { ...value }, + embedded_python: pythonDiagnostic, + }; + if (!python.ok && python.error.reason === "cancelled") + throw new AnalysisCancelledError("doctor", { + cause: python.error.cause, + diagnostics: { hopper_demo_runtime: details }, + }); + if (selection.ok && pythonFailure === undefined) return { name: "hopper-demo-runtime", ok: true, classification: "healthy", detail, - details: { private_display: { ...value } }, + details, }; const missingDependency = value.failure_code === "runtime_dependency_unavailable" || - value.failure_code === "x11_authorization_failed"; + value.failure_code === "x11_authorization_failed" || + (python.ok && python.value.library.status === "unavailable"); + const pythonRemediation = + pythonFailure === undefined + ? undefined + : pythonFailure.reason === "runtime_unavailable" + ? pythonFailure.message + + " On a supported system run rea setup --install-hopper, then rerun rea doctor --provider hopper. This checks the system interpreter; a custom Hopper Python interpreter needs its own matching shared runtime." + : pythonFailure.reason === "configuration_unknown" + ? pythonFailure.message + + " Inspect this interpreter's shared-runtime configuration, then rerun rea doctor --provider hopper." + : pythonFailure.message + + " Retry rea doctor --provider hopper and inspect /usr/bin/python3 if its runtime remains unknown."; + const displayRemediation = selection.ok + ? undefined + : value.failure_code === "x11_socket_directory_unusable" + ? "Enable unprivileged user and mount namespaces, or provide a writable mode-1777 /tmp/.X11-unix directory. Do not mutate the current host mount with sudo; then rerun rea doctor --provider hopper." + : value.failure_code === "runtime_dependency_unavailable" || + value.failure_code === "x11_authorization_failed" + ? "Install Xvfb, xauth, Python 3, libX11, and libXtst, then rerun rea doctor --provider hopper." + : "Inspect the private-display diagnostic, correct the reported local condition, and rerun rea doctor --provider hopper."; return { name: "hopper-demo-runtime", ok: false, - classification: missingDependency ? "missing_dependency" : "config_drift", + classification: missingDependency + ? "missing_dependency" + : selection.ok + ? "unknown" + : "config_drift", detail, - details: { private_display: { ...value } }, - remediation: - value.failure_code === "x11_socket_directory_unusable" - ? "Enable unprivileged user and mount namespaces, or provide a writable mode-1777 /tmp/.X11-unix directory. Do not mutate the current host mount with sudo; then rerun rea doctor --provider hopper." - : missingDependency - ? "Install Xvfb, xauth, Python 3, libX11, and libXtst, then rerun rea doctor --provider hopper." - : "Inspect the private-display diagnostic, correct the reported local condition, and rerun rea doctor --provider hopper.", + details, + remediation: [pythonRemediation, displayRemediation] + .filter((message) => message !== undefined) + .join(" "), }; };