PH-22.1 — static native launcher for mythic-vibe-cli that requires no Python pre-installed on the host.
A small (~3-5 MB) Rust binary that:
- First run: downloads python-build-standalone into a per-user cache (~30 MB), creates a venv, installs
mythic-vibe-clifrom PyPI, then execs the CLI. - Subsequent runs: short-circuits to the cached venv and execs the CLI directly (~50 ms overhead vs invoking the cached
mythic-vibebinary directly).
Operators get a single static binary for environments where pip install isn't an option (sandboxed boxes, locked-down workstations, kiosks). Compared to the PyInstaller / Nuitka binaries from PH-21.2 / PH-21.3:
| Property | PyInstaller / Nuitka | Launcher (PH-22.1) |
|---|---|---|
| Binary size | ~15-25 MB / ~8-15 MB | ~3-5 MB |
| First-run cold start | ~250-500 ms / ~80-150 ms | ~30-60 s (interpreter download) |
| Subsequent cold start | ~250-500 ms / ~80-150 ms | ~150-250 ms |
Optional extras (ai, tui, ux, otel) |
❌ stdlib-only | ✅ pip-installable into the cached venv |
| Network required for first run | ❌ | ✅ |
The launcher's strength is flexibility: operators can pip install mythic-vibe-cli[ai] into the cached venv to add extras. The PyInstaller / Nuitka binaries embed only the stdlib base.
<cache_root>/
├── py-3.12.7/ # python-build-standalone interpreter
│ └── python/
│ └── bin/python3 # the cached interpreter
└── venv/ # operator-extensible venv
└── bin/mythic-vibe # the actual CLI entry point
<cache_root> is platform-default per dirs::cache_dir():
- Linux:
~/.cache/mythic-vibe-launcher/ - macOS:
~/Library/Caches/mythic-vibe-launcher/ - Windows:
%LOCALAPPDATA%\mythic-vibe-launcher\Cache\
Operators can redirect via the MYTHIC_LAUNCHER_CACHE env var (useful for CI runners that want a workspace-local cache, or sandboxed environments where the platform cache dir isn't writable).
| Variable | Default | Purpose |
|---|---|---|
MYTHIC_LAUNCHER_CACHE |
platform-default | Redirect the cache root (CI / sandboxed installs / shared caches) |
MYTHIC_LAUNCHER_REQUIRE_SHA |
unset (lenient) | Set to 1 to require an entry in PBS_EXPECTED_SHA256 for the host triple — refuses to extract any unverified archive |
MYTHIC_LAUNCHER_MIRRORS |
unset | Comma-separated additional download URLs tried after the canonical GitHub URL. Supports __VERSION__, __TAG__, __TRIPLE__ placeholders. Example: https://artifacts.example.com/pbs/__TAG__/cpython-__VERSION__-__TRIPLE__-install_only.tar.gz |
# Inside packaging/launcher/:
cargo build --release
# The binary lands at:
ls target/release/mythic-vibe-launcher
# Run it:
./target/release/mythic-vibe-launcher --versionThe CI workflow (.github/workflows/release-launcher.yml) builds per OS matrix on tag push and attaches the binaries to the GitHub Release.
# Find the cached venv:
LAUNCHER_VENV="$(dirs --cache 2>/dev/null || echo ~/.cache)/mythic-vibe-launcher/venv"
# Activate it and install:
"$LAUNCHER_VENV/bin/pip" install "mythic-vibe-cli[ai,tui]"The extras land in the same venv the launcher execs against, so they're available next run.
What works today:
- ✅ Crate compiles on Rust 1.74+ (the rust-version pin in Cargo.toml).
- ✅ Lifecycle stages (resolve cache → ensure interpreter → ensure venv → exec) are split into named functions for testability.
- ✅ Per-arch
python-build-standaloneURL resolution covers x86_64 + aarch64 on Linux + macOS, plus x86_64 on Windows. - ✅
MYTHIC_LAUNCHER_CACHEenv override. - ✅ tar.gz + tar.zst archive extraction.
- ✅ Unix
execvso signals + ttys + exit codes pass through cleanly.
What was shipped after the foundation level:
- ✅ SHA256 verification of downloaded archives (PH-23.4, 2026-05-05). The
verify_archive_sha256function insrc/main.rshashes the downloaded archive bytes and compares against the per-arch table atPBS_EXPECTED_SHA256. Three branches: matching expected SHA → ok; mismatch → hard error; no expected SHA in table → log a warning + continue (lenient default), unlessREQUIRE_VERIFIED_CHECKSUMS = trueorMYTHIC_LAUNCHER_REQUIRE_SHA=1flips strict mode. The table starts withNonefor every arch — populate it by runningpython tools/fetch_pbs_checksums.pyand pasting the rendered table intosrc/main.rs. Each PBS release tag bump invalidates the table; re-run the script. - ✅ First-run UX polish (PH-23.6, 2026-05-05). Three subcomponents:
- Streaming progress bar via the
indicatifcrate. The download reads in 64 KiB chunks; the bar showsbytes/total_bytes (eta)whenContent-Lengthis present, or a bytes-read spinner when it isn't. Operators no longer see a 30-60 s "hang" — they see continuous progress. - Retry with exponential backoff. Each URL is attempted up to
MAX_DOWNLOAD_ATTEMPTS(4) times, withRETRY_BACKOFF_BASE_SECS × 2^attemptbetween tries (1 s → 2 s → 4 s = 7 s total worst-case for a single mirror). Transient network errors recover automatically. - Mirror fail-over. The launcher tries the canonical GitHub Releases URL first, then any URLs from the comma-separated
MYTHIC_LAUNCHER_MIRRORSenv var. Each mirror entry supports__VERSION__,__TAG__, and__TRIPLE__placeholders for substitution. Operators with internal artifactory mirrors set the env var at job startup; the public default is GitHub-only.
- Streaming progress bar via the
What's deferred to a future session:
⚠️ Populate thePBS_EXPECTED_SHA256table with real SHA values (~5 min, requires network reach). The verification path exists; the SHAs are the missing piece. Runpython tools/fetch_pbs_checksums.pyand paste the output. Once every arch has a real SHA, flipREQUIRE_VERIFIED_CHECKSUMStotrueso a missing row is a hard error instead of a warning.⚠️ Wheel version pinning. Todaypip install mythic-vibe-cliresolves the latest version on PyPI. A future session adds--upgrade-strategy only-if-neededdefaulting +MYTHIC_LAUNCHER_CLI_VERSIONenv override for operators who want a pinned version.⚠️ Sigstore verification of the downloaded wheel. PH-21.5 publishes Sigstore signatures for every PyPI artifact; the launcher could verify them at install time. Adds the sigstore-rs dep + ~50 lines of verification code.⚠️ Offline cache pre-population. Amythic-vibe-launcher --pre-populateflag that downloads the interpreter + wheel without execing the CLI. Lets ops bake a cache into a base image.
Both can produce static binaries with embedded HTTP + archive extraction. Rust was chosen because:
- Smaller binaries. Rust's
opt-level = "z" + lto + strip + panic=abortproduces ~3 MB binaries; equivalent Go is ~7-9 MB after-ldflags="-s -w". - Cargo's dependency hygiene. Pinned
Cargo.lock+ thecargo auditecosystem give us a clean supply-chain story. execvfirst-class. Unix exec semantics work cleanly throughstd::os::unix::process::CommandExt::exec. Go'ssyscall.Execworks but feels less idiomatic.- Sigstore-rs. Future Sigstore verification has a mature Rust client (
sigstorecrate) — Go has cosign but it's cli-shaped, not lib-shaped.
The PyInstaller + Nuitka binaries (PH-21.2 + PH-21.3) embed the stdlib-only base — operators who need [ai] or [tui] extras can't use them. The launcher fills that gap by giving operators a static binary today + the ability to extend it via pip into the cached venv tomorrow.
Three operator profiles:
- Sandboxed-but-online operator (the launcher's sweet spot). Network is available, no Python pre-installed, may or may not need extras later.
- Air-gapped operator. No network. Use the offline wheelhouse path documented in
docs/INSTALL.mdinstead. - Container-friendly operator. Use the OCI image (PH-21.1) instead — same outcome, no host filesystem cache.
The launcher is one of three strategies, not a replacement for the others.