Addresses the third review round on PR #481. - `bot-bottle doctor` now checks `is_backend_ready()` (a full backend status() probe: daemon reachable, network pool present, KVM usable) instead of the cheap PATH-only `is_backend_available()`. A host with a stopped Docker daemon or half-configured Firecracker no longer reports `ok: backend` / exit 0 when `start` can't actually work; each not-ready backend prints its own diagnostics, and doctor passes only if at least one backend is ready. - `install.sh` resolves the pip `--user` scripts directory from the interpreter (`sysconfig.get_path("scripts", get_preferred_scheme("user"))`) instead of hardcoding `~/.local/bin`, which is wrong on a python.org macOS interpreter (`~/Library/Python/<X.Y>/bin`). The PATH guidance now prints the actual directory. Tests: doctor tests mock `is_backend_ready` (the readiness contract) and cover the not-ready → fail path; a new install-script test drives the macOS `osx_framework_user` scheme and asserts it resolves a non-~/.local/bin directory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.6 KiB
PRD prd-new: Quick install script
- Status: Draft
- Author: claude
- Created: 2026-07-25
- Issue: #197
Summary
Add a proper Python package distribution (pyproject.toml with a
bot-bottle entry point) plus a thin install.sh bootstrapper, so users
can install bot-bottle with a single command instead of cloning the repo
and invoking cli.py directly. A new bot-bottle doctor subcommand
verifies host prerequisites after install.
Problem
There is currently no install path for new users. The only way to run
bot-bottle is to clone the repo and invoke ./cli.py. This blocks any
public demo: readers want curl | sh or pipx install, not a manual
clone-and-configure flow. There is also no single command that tells a
user whether their host is actually ready to run a bottle.
Goals / Success Criteria
curl -fsSL <raw-url>/install.sh | shleaves a workingbot-bottlecommand on PATH.- Python-native users can install with
pipx install bot-bottleoruv tool install bot-bottle(once published) — or from a local checkout today. install.shvalidates prerequisites (Python ≥ 3.11), creates the~/.bot-bottle/config tree, installs the package, and runsbot-bottle doctor. It never installs Docker or a VM backend silently and never usessudo.install.shis idempotent — safe to re-run.bot-bottle doctorreports Python version, backend readiness, and config-dir presence, exiting non-zero when a hard prerequisite is unmet.- The package keeps zero runtime pip dependencies (stdlib-only,
matching the existing constraint in
AGENTS.md).
Non-goals
- Bundling a Python runtime or producing a standalone binary.
- Automatic Docker / VM-backend installation.
- Plugin-architecture changes (issue #197 floats a containerized-plugin direction; that's a separate feature).
- Publishing to a package index in this PR — the package structure is the deliverable; publishing is a follow-up step.
Design
Package structure (pyproject.toml)
Fill out the previously-stub pyproject.toml with project metadata, a
console-script entry point, and package-data for the non-Python assets the
runtime reads from inside the package:
[project]
name = "bot-bottle"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []
[project.scripts]
bot-bottle = "bot_bottle.cli:main"
bot_bottle.cli:main already exists (the cli.py shim calls it), so no
refactor of the entry point is needed. package-data ships the non-Python
assets that live inside the package (egress_entrypoint.sh, the contrib
Dockerfiles, the firecracker netpool defaults, the macos-container init
script).
Self-contained wheel (build resources)
The gateway / infra / orchestrator images are built from a Docker (or Apple
container) build context that must contain the bot_bottle package,
pyproject.toml, and the root-level Dockerfiles as siblings. Several
modules used to locate that context by walking __file__'s parents to the
repo root (_REPO_ROOT = Path(__file__)…parents[N]) and reading
Dockerfile.gateway, nix/firecracker-netpool.nix, and
scripts/firecracker-netpool.sh from it. In an installed wheel the package
lives in site-packages with no repo root above it, so those reads fail —
doctor passes but start / backend setup breaks.
Fix: a single resolver, bot_bottle/resources.py.
build_root()returns a directory shaped like a repo root (hasbot_bottle/,pyproject.toml, the Dockerfiles,nix/,scripts/). In a checkout it's the repo root itself — unchanged behavior. From an installed wheel it stages a copy under the app-data dir, keyed by a content digest of the installed package + bundled resources (not the distribution version): the installer defaults to a git branch andpipx install --forcewhileversionstays0.1.0, so a version key would reuse a previous commit's tree — the digest key re-stages instead. Staging is concurrency-safe: a file lock serializes it, each writer builds into a private temp dir, and the finished tree is published with an atomic rename (never populating a shared path another process might read).- The root-level resources are shipped inside the wheel under
bot_bottle/_resources/by asetup.pybuild_pystep (kept in sync withresources.BUNDLED_RESOURCES);MANIFEST.inincludes them in the sdist. - Every former
_REPO_ROOT/_REPO_DIRcall site now derives fromresources: the docker/macos agent-image launch, each backend'sorchestrator/gateway/infraservice, firecrackerinfra_vm/infra_artifact/setup, and the sharedgatewaybuild context. So checkout and wheel installs share one downstream path.
Verification: test_resources exercises both layouts — including the staged
wheel context, a re-stage when package content changes at the same version,
and a rebuild of a partial (crashed) stage. test_wheel_install builds the
wheel, installs it into an isolated venv, and asserts bot-bottle doctor
runs and build_root() produces a valid context; build is in
requirements-dev.txt so it runs in CI, and a build/install failure fails
the test (it does not skip). Running start end-to-end still needs a
Docker/KVM host (CI), not a source checkout.
install.sh
A POSIX sh bootstrapper that:
- Checks
python3is present and ≥ 3.11; exits with a clear message otherwise. - Checks
gitwhen installing agit+spec, and — when falling back to pip — that pip is usable and the interpreter isn't externally managed (PEP 668), pointing at pipx otherwise. - Creates
~/.bot-bottle/{agents,bottles,contrib}. - Installs via
pipxif available, elsepython3 -m pip install --user. The spec defaults to the git URL and is overridable viaBOT_BOTTLE_INSTALL_SPEC(used by tests / local installs). - Locates the
bot-bottleentry point: PATH first, else the interpreter's own user-scheme scripts dir resolved viasysconfig(~/.local/binon Linux,~/Library/Python/<X.Y>/binon a python.org macOS interpreter — not hardcoded). - Runs
bot-bottle doctorand reports the result.
It is idempotent and never calls sudo.
bot-bottle doctor
A new store-free subcommand (no DB migration required) that checks and reports:
- python — interpreter version (hard requirement: ≥ 3.11).
- backend — at least one backend ready on this host
(macos-container / firecracker / docker), via
is_backend_ready()— a full backendstatus()probe (daemon reachable, network pool present, KVM usable), not a PATH-only check: a stopped daemon or half-configured backend must not reportokwhenstartcan't work. Each not-ready backend prints its own diagnostics. Hard requirement. - config — whether
~/.bot-bottle/exists (advisory only;startprovisions on first run).
Exits 0 when both hard requirements pass, non-zero otherwise.
Testing strategy
- Unit test
bot-bottle doctorsuccess/failure paths with backend readiness (is_backend_ready) and Python version mocked, including the available-but-not-ready → fail case. - Unit test that
pyproject.tomlparses, declares the entry point and an emptydependencieslist, and that everypackage-dataglob resolves to a file that exists on disk (guards against drift). - Unit test that
install.shis executable, POSIX-ish (set -eu), never callssudo, and runsdoctorafter install.
Open questions
- Should
versionbe derived from a git tag at build time (e.g.hatch-vcs) or kept static? Static (0.1.0) is simpler for now. - Publishing target (PyPI vs. a self-hosted index) is deferred.