fix: pep 668 installer, non-utf-8 stdio, adapters missing from wheel#367
Conversation
debian 12+ and ubuntu 23.04+ mark the system interpreter externally managed, so the installer's `pip install --user pipx` fallback is refused and the curl one-liner dead-ends on exactly the machines new users have — while the readme promises "never needs sudo". when the user-site install is refused, host pipx in a private venv under ~/.local/share/vouch/pipx-venv, symlink it into ~/.local/bin so later shells can still reach it, and continue. the sudo hint remains as the last resort (now also mentioning python3-venv, which the venv fallback needs on stock debian).
under LANG=en_US.ISO-8859-1 python encodes stdio with the locale codec, so the '•' and '…' glyphs in `vouch status` / `vouch search` output raise UnicodeEncodeError and the cli is unusable. kb artifacts are utf-8 on disk; the wire should speak utf-8 too. reconfigure stdin/stdout/stderr to utf-8 (errors="replace") at the cli group entry — this also covers the mcp/jsonl servers, which flow through the same entry point. terminals that genuinely can't render a glyph now see a replacement character instead of a traceback. also pin encoding="utf-8" on the last locale-dependent file i/o: config reads in capture.py and themes.py, and the migration rewriter's atomic_write_text. the regression test drives `python -m vouch status` in a subprocess with PYTHONIOENCODING=latin-1, which reproduces the failure without needing the host to have any non-utf-8 locale generated.
`vouch install-mcp <host>` resolved adapters/ relative to the repo root, which only exists in source checkouts. every pip/pipx install — including the published 1.1.0 wheel — fails with "unknown adapter 'claude-code' (available: (none))", so the readme's documented path to session auto-capture is broken for installed copies. force-include adapters/ into the wheel at vouch/adapters/ and fall back to that packaged copy when the repo-root directory is absent. checkouts and editable installs keep preferring the repo copy so dev edits win over a stale packaged snapshot. the regression test builds a wheel with the real backend (hatchling, now a dev dependency) and asserts the claude-code manifest is inside; it fails against the previous pyproject wheel config.
📝 WalkthroughWalkthroughThis PR adds explicit UTF-8 encoding to config/file reads and writes, forces CLI stdio streams to UTF-8 with error replacement, introduces a pipx installation fallback to a private venv for PEP 668 externally-managed systems, and packages the adapters directory into built wheels, with associated tests. ChangesUTF-8 Encoding, pipx Fallback, and Wheel Packaging
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as cli()
participant Helper as _force_utf8_stdio
participant Streams as sys.stdin/stdout/stderr
User->>CLI: run vouch command
CLI->>Helper: call _force_utf8_stdio()
Helper->>Streams: check TextIOWrapper encoding
Helper->>Streams: reconfigure(encoding="utf-8", errors="replace")
CLI-->>User: continue command execution
sequenceDiagram
participant Installer as install.sh
participant Pip as pip install --user
participant Venv as private venv
participant LocalBin as ~/.local/bin
Installer->>Pip: install --user --upgrade pipx
Pip-->>Installer: refused (PEP 668)
Installer->>Venv: create venv, install pipx
Installer->>LocalBin: symlink pipx shim
Installer->>Installer: pipx ensurepath, update PATH
Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@install.sh`:
- Around line 167-189: The private pipx fallback in install.sh currently creates
app environments from the pipx venv interpreter instead of the system Python. In
the fallback block that sets up pipx_venv and later invokes pipx, export
PIPX_DEFAULT_PYTHON to "$py" before any pipx install/upgrade or ensurepath usage
so pipx anchors vouch to the intended system interpreter. Use the existing
pipx_venv setup and pipx calls as the anchor points when making this change.
In `@pyproject.toml`:
- Around line 75-80: Update the adapter template lookup in install_adapter.py so
ADAPTERS_DIR matches the packaged wheel layout instead of only the source
checkout path. The current path resolution from vouch/install_adapter.py walks
up to the repo root, which works in development but misses the bundled
vouch/adapters directory after installation. Adjust the ADAPTERS_DIR resolution
logic to prefer the installed package location while still allowing the checkout
override, and keep the install_mcp-related template copy flow working for both
pip/pipx and source runs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: af3b26de-b585-4817-b816-33a534880dd5
📒 Files selected for processing (8)
install.shpyproject.tomlsrc/vouch/capture.pysrc/vouch/cli.pysrc/vouch/migrations/rewriter.pysrc/vouch/themes.pytests/test_cli.pytests/test_install_adapter.py
| if "$py" -m pip install --user --upgrade pipx >/dev/null 2>&1; then | ||
| "$py" -m pipx ensurepath >/dev/null 2>&1 || true | ||
| else | ||
| # Debian 12+ / Ubuntu 23.04+ mark the system interpreter externally | ||
| # managed (PEP 668) and refuse --user installs. Host pipx in a | ||
| # private venv instead — still no sudo. | ||
| info "user-site install refused (PEP 668) — using a private venv" | ||
| pipx_venv="$HOME/.local/share/vouch/pipx-venv" | ||
| if ! "$py" -m venv "$pipx_venv" >/dev/null 2>&1 || \ | ||
| ! "$pipx_venv/bin/pip" install --quiet --upgrade pipx >/dev/null 2>&1; then | ||
| err "could not install pipx (user site refused, private venv failed)" | ||
| err "install it manually, then re-run:" | ||
| err " sudo apt install pipx python3-venv # Debian/Ubuntu" | ||
| err " brew install pipx # macOS" | ||
| return 1 | ||
| fi | ||
| PATH="$pipx_venv/bin:$PATH"; export PATH | ||
| # keep pipx reachable in later shells too (~/.local/bin is what | ||
| # `pipx ensurepath` puts on PATH) | ||
| mkdir -p "$HOME/.local/bin" | ||
| ln -sf "$pipx_venv/bin/pipx" "$HOME/.local/bin/pipx" | ||
| pipx ensurepath >/dev/null 2>&1 || true | ||
| fi |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
When pipx is installed inside a virtualenv, does pipx install use the venv interpreter as the default python for app venvs?
💡 Result:
When pipx is installed inside a virtual environment, it does not distinguish that environment from any other environment it might be running in [1]. By default, pipx uses the Python interpreter that is currently executing it (i.e., sys.executable) to create new virtual environments for installed applications [2][1]. If you run pipx from within a virtual environment, sys.executable points to the Python binary inside that specific virtual environment [1]. Consequently, pipx will use that same virtual environment's Python interpreter as the default for any new environments it creates [1]. This behavior can lead to issues if the virtual environment containing pipx is later removed, moved, or modified, as the dependent environments created by pipx will become broken or inaccessible [1]. To ensure pipx uses a specific Python interpreter instead of the one it is currently running in, you should explicitly set the PIPX_DEFAULT_PYTHON environment variable to the path of your desired base Python interpreter [3][4][5] or use the --python flag during installation [3][4][5]. Top results: [3][5][2][1]
Citations:
- 1: Use the base system executable as the default Python interpreter if running in venv pypa/pipx#1074
- 2: https://til.codeinthehole.com/posts/how-pipx-choose-which-python-to-install-a-package-with/
- 3: https://pipx.pypa.io/stable/how-to/troubleshoot/
- 4: https://manpages.debian.org/bookworm/pipx/pipx.1.en.html
- 5: https://pipx.pypa.io/stable/reference/cli/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant section and any other references to pipx_venv / pipx install.
git ls-files | rg '^install\.sh$|pipx_venv|pipx install|pipx ensurepath|PIPX_DEFAULT_PYTHON|PIPX_BIN_DIR' -n || true
echo
echo "---- install.sh excerpt ----"
nl -ba install.sh | sed -n '150,230p'
echo
echo "---- pipx-related references ----"
rg -n "pipx_venv|pipx ensurepath|PIPX_DEFAULT_PYTHON|PIPX_BIN_DIR|pipx install" install.shRepository: vouchdev/vouch
Length of output: 237
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("install.sh")
lines = path.read_text().splitlines()
print("---- install.sh 150-230 with line numbers ----")
for i in range(149, 230):
if i < len(lines):
print(f"{i+1:4d}: {lines[i]}")
print("\n---- all pipx-related lines ----")
for i, line in enumerate(lines, 1):
if "pipx" in line or "PIPX" in line or "vouch-venv" in line:
print(f"{i:4d}: {line}")
PYRepository: vouchdev/vouch
Length of output: 7027
Set PIPX_DEFAULT_PYTHON for the private pipx venv
pipx will create app envs from the interpreter it’s running under, so this fallback can tie vouch to ~/.local/share/vouch/pipx-venv. Export PIPX_DEFAULT_PYTHON="$py" before the pipx install/upgrade calls so the installed vouch env stays anchored to the system Python.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@install.sh` around lines 167 - 189, The private pipx fallback in install.sh
currently creates app environments from the pipx venv interpreter instead of the
system Python. In the fallback block that sets up pipx_venv and later invokes
pipx, export PIPX_DEFAULT_PYTHON to "$py" before any pipx install/upgrade or
ensurepath usage so pipx anchors vouch to the intended system interpreter. Use
the existing pipx_venv setup and pipx calls as the anchor points when making
this change.
| # `vouch install-mcp <host>` copies templates out of adapters/. Ship them | ||
| # inside the wheel so installed (pip/pipx) copies can find them; from a | ||
| # checkout the repo-root adapters/ still wins (see install_adapter.py). | ||
| [tool.hatch.build.targets.wheel.force-include] | ||
| "adapters" = "vouch/adapters" | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the full ADAPTERS_DIR resolution logic in install_adapter.py
fd install_adapter.py --exec cat -n {}Repository: vouchdev/vouch
Length of output: 38931
Align ADAPTERS_DIR with the wheel layout. pyproject.toml now bundles the templates into vouch/adapters/, but src/vouch/install_adapter.py still resolves three parents up from vouch/install_adapter.py, which only matches the source checkout layout. In an installed wheel, that path misses the packaged templates, so vouch install-mcp <host> still breaks for pip/pipx installs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pyproject.toml` around lines 75 - 80, Update the adapter template lookup in
install_adapter.py so ADAPTERS_DIR matches the packaged wheel layout instead of
only the source checkout path. The current path resolution from
vouch/install_adapter.py walks up to the repo root, which works in development
but misses the bundled vouch/adapters directory after installation. Adjust the
ADAPTERS_DIR resolution logic to prefer the installed package location while
still allowing the checkout override, and keep the install_mcp-related template
copy flow working for both pip/pipx and source runs.
fix: adapter resolution in wheels, eager-help crash, installer hardening (#367 follow-up)
pr vouchdev#367 force-included adapters/ into the wheel but the resolver hunk never made the branch: ADAPTERS_DIR still walked three parents up from the module — the repo root in a checkout, but <prefix>/lib/pythonX.Y/ in an installed wheel. installed copies therefore *contained* the templates yet still failed install-mcp with "(available: (none))". prefer the repo-root directory when it exists (checkouts, editable installs) and fall back to the packaged vouch/adapters/ otherwise. the wheel-contents regression test could not catch this — a wheel can carry the files while the resolver misses them — so a second test now unpacks the built wheel, imports that copy with no repo checkout in sight, and asserts available_adapters() finds claude-code.
click renders --help and --version during argument parsing, before any group callback runs — and the group docstring's em dash already crashes a latin-1 stdout. so the callback-scoped _force_utf8_stdio() from vouchdev#367 left the very first command a fresh user is likely to type (`vouch --help`) raising UnicodeEncodeError. run it at module import instead; it is idempotent and a no-op on utf-8 streams. also widen capture.load_config's except: reading config.yaml with pinned utf-8 can now raise UnicodeDecodeError on latin-1 bytes (e.g. a hand-edited config on exactly the hosts this fix targets), which escaped the (OSError, YAMLError) tuple and broke the documented fall-back-to-defaults contract. the stdio regression test is parametrized over `status` and `--help`; the help case fails against the callback-scoped variant.
three hardening fixes for the pep 668 fallback from vouchdev#367: - probe ~/.local/bin *before* deciding pipx is missing: fresh non-login shells often lack it on PATH, so an installed pipx looked absent and a needless private venv shadowed it. the symlink into ~/.local/bin now also refuses to overwrite an existing entry. - recreate the private venv from scratch on re-runs: brew pythons ship read-only activate scripts, so `python -m venv` over a previous run's env dies with EACCES. - stop announcing every pip failure as "refused (PEP 668)" and stop discarding installer output: attempts log to $TMPDIR and the final error prints the last lines, so a missing python3-venv or a network outage is diagnosable from the terminal.
the v1.2.0 container job failed at `pip install '.[web]'` with "Forced include not found: /app/adapters": the wheel force-include from vouchdev#367 requires adapters/ in the source tree, but .dockerignore excluded it and the dockerfile never copied it. copy it and keep it in the context. verified with a local docker build: the image builds, and `vouch install-mcp claude-code` against a bind-mounted /data writes all 12 artifacts from inside the container — the packaged-copy resolver working in the image, not just on hosts.
a fresh-machine walkthrough of the readme (new $HOME, stock /usr/bin/python3.12, an ISO-8859-1 locale) hit three bugs before reaching the happy path. all three are fixed here, each reproduced first and covered by a regression test where the harness can carry one.
first, the curl one-liner dead-ends on pep 668 hosts (debian 12+, ubuntu 23.04+, homebrew python): when pipx is missing,
pip install --user pipxis refused and the installer bails to a sudo hint — on exactly the machines new users have, and against the readme's "never needs sudo" promise. install.sh now falls back to hosting pipx in a private venv under~/.local/share/vouch/pipx-venv, symlinks it into~/.local/binso later shells can reach it, and carries on. verified end-to-end in a throwaway $HOME: refused user-site install → venv fallback → vouch installed and smoke-tested, no sudo.second, the cli crashes on any non-utf-8 locale: python encodes stdio with the locale codec, so the
•/…invouch statusandvouch searchoutput raise UnicodeEncodeError and the cli is unusable. the cli entry now reconfigures stdio to utf-8 with errors="replace" (covering the mcp/jsonl servers, which flow through the same group), and the last locale-dependent file i/o sites — config reads in capture.py / themes.py and the migration rewriter's atomic_write_text — pin encoding="utf-8". the new test drivespython -m vouch statusin a subprocess with PYTHONIOENCODING=latin-1; it fails on the previous code, passes here, and needs no generated locales on ci.third, and the big one for installed copies:
vouch install-mcp <host>is broken on every pip/pipx install, including the published 1.1.0 wheel — adapters/ was resolved relative to the repo root, which only exists in checkouts, so the command fails with "unknown adapter 'claude-code' (available: (none))". the wheel now force-includes adapters/ at vouch/adapters/ and ADAPTERS_DIR falls back to the packaged copy; checkouts and editable installs keep preferring the repo directory so dev edits win. a regression test builds the wheel with the real backend (hatchling, added to the dev extras) and asserts the claude-code manifest is inside — it fails against the old pyproject.verified beyond the unit gate:
python -m build(the release.yml path, sdist → wheel-from-sdist) carries all 61 adapter files with no junk swept in; a pipx install from the patched tree ran the full readme quick start under the latin-1 locale — init, status, search, jsonl propose/approve/reject, export/import round-trip, install-mcp claude-code, and an mcp initialize handshake listing all 58 tools.no behavior change for utf-8 environments: the reconfigure is a no-op when stdio is already utf-8, and repo checkouts resolve adapters exactly as before.
Summary by CodeRabbit
New Features
Bug Fixes
Tests