Skip to content

feat(cookbook): one-click venv setup on server profiles#5694

Closed
catagris wants to merge 1928 commits into
odysseus-dev:devfrom
catagris:feat/cookbook-venv-setup
Closed

feat(cookbook): one-click venv setup on server profiles#5694
catagris wants to merge 1928 commits into
odysseus-dev:devfrom
catagris:feat/cookbook-venv-setup

Conversation

@catagris

@catagris catagris commented Jul 22, 2026

Copy link
Copy Markdown

Summary

Pip installs and serve engines need a working Python environment on the target server — and "working" means two things this PR now handles end-to-end: PEP 668 distros refuse system-python installs, and the serving stack needs an interpreter in a supported range (>=3.10,<3.14 — numba and friends enforce it with build-time guards, not metadata pip could act on, so installs previously burned minutes before failing). The venv is treated as the foundation of the Dependencies tab:

  • "Python environment" row pinned above the package sections (remote servers), driven by a new check mode on the setup route: shows Ready (venv on a supported interpreter — profile wired automatically, even for manually-created venvs), Fix (venv exists on an out-of-range interpreter — rebuilds it; only ever replaces a dir containing pyvenv.cfg), or Install (no venv — names which interpreter will be used, or what to install when none qualifies).
  • Version-aware creation in POST /api/cookbook/setup-venv: picks the newest interpreter inside SERVING_PYTHON_RANGE (python3.13python3.10, then bare python3 if in range), falls back to uv venv --seed --python 3.13 (uv fetches a standalone build, no sudo), else returns an actionable error naming the system python found and what to install. Runs via sh -lc (non-POSIX login shells); Debian's stripped-ensurepip failure names python3-venv.
  • Venv button on each server entry in Settings → Servers (next to Check/Key, same pill styling, monochrome inline SVG) — the low-level tool, sharing the same version-aware backend; reports the interpreter used.
  • uv and cargo as installable System dependency rows — uv is this feature's own fallback; cargo is a build prereq in the cmake/gcc class (pip deps like outlines_core need it for sdist builds). Per-OS package mapping included (Arch/brew ship the toolchain as rust).
  • System rows now show their install command: the passwordless-sudo failure toast says "run the shown install command" — previously that command rendered nowhere for standalone System rows (tmux included). The packages endpoint now attaches the target-OS-resolved command, the row renders the same command box build-prereq rows get, and the failure toast picks it up with its Copy-command action.

Target branch

  • This PR targets dev, not main.

Linked Issue

Fixes #5691

Type of Change

  • New feature (non-breaking — adds new behaviour)

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app and verified the change works end-to-end on a real Arch server whose system python (3.14) is outside the supported range: with no suitable interpreter, the row said so and Install returned the actionable error naming uv; after installing uv (via the new System row's surfaced command), Install fetched Python 3.13, created the venv, and wired the profile; a subsequent hf_transfer install landed in the 3.13 venv and sglang[all] proceeded past the interpreter guards. Full test suite passes (4,681) including route tests for interpreter selection, check mode, the no-interpreter error, path validation, and the ensurepip hint.

How to Test

  1. Dependencies tab with a remote server selected: the "Python environment" row shows live state. On a host with only an unsupported python and no uv: Install → error names uv/distro packages; the uv System row below shows the exact command for the target's OS.
  2. After installing uv (or with a distro python3.10–3.13 present): Install → venv created on a supported interpreter, toast names the version, env/path appear on the server profile, row flips Ready.
  3. Break it on purpose: create the venv with an out-of-range python — the row shows Fix and rebuilds it on a supported interpreter.
  4. Settings → Servers → Venv button does the same version-aware creation for the profile's path.
  5. Any pip dependency row now installs into the venv; the cargo row surfaces its per-OS install command when clicked without passwordless sudo.

Visual / UI changes — REQUIRED if you touched anything that renders

  • Screenshot or short clip of the change in the running app, attached below.
  • Style match: the environment row reuses the existing .cookbook-dep-row / .cookbook-dep-tag / memory-item-* classes; the Venv button reuses the exact Check/Key pill markup; monochrome inline SVG; no emoji; no new colors, fonts, or spacing values.
  • No new component patterns — a dep row, a tag button, and a third sibling of Check/Key, wired in the existing handlers.
  • I am not an LLM agent submitting a bulk PR. Single feature grown from Cookbook: one-click venv creation on server profiles (Venv button next to Check/Key) #5691, developed and verified against my own deployment.

Screenshots / clips

Screenshots of the Python environment row (Ready / Install states) and the Venv button attached in a comment below.

🤖 Generated with Claude Code

pewdiepie-archdaemon and others added 30 commits June 27, 2026 21:12
Keep an unhealthy MemoryVectorStore instance available for health reporting instead of discarding it as disabled. This lets health checks report a degraded/down vector-store state while preserving focused regression coverage for initializer behavior.
0xLeathery and others added 23 commits July 11, 2026 21:15
* fix(db): restrict data/app.db to 0600

app.db holds bearer-token hashes, bcrypt password hashes, and encrypted
provider keys but was created under the default umask (0644 -> world-readable),
unlike .app_key/vault/integrations which are already 0600 via safe_chmod.

init_db() now chmods the SQLite file to 0600 right after create_all (POSIX
only; no-op on Windows, skipped for Postgres / in-memory). Unconditional and
idempotent, so it also re-locks already-deployed 0644 installs on next
startup. The transient rollback journal inherits 0600 from the parent file at
creation - no sidecar handling needed; -wal/-shm don't exist until WAL is
enabled (#4409 C4) and inherit the same mode then.

Satisfies Rule B, unblocking #4413 and the vault/integration secret moves.
Mirrors src/secret_storage.py:43-45.

Verified: security + DB-permission suites pass; 6 pre-existing visual_report
failures (missing markdown/nh3 deps) are unrelated.

Closes #4407

* fix(db): harden SQLite path parsing and re-lock sidecars

Address review feedback on #4420.

P2: derive the file to chmod from engine.url (SQLAlchemy's parsed URL)
via _sqlite_db_path(), instead of DATABASE_URL.replace("sqlite:///", "").
A driver-qualified URL (sqlite+pysqlite://) or one carrying query args
(?cache=shared) previously slipped past the prefix check / string slice
and left the DB world-readable; the parsed path resolves correctly and
drops the query.

P3: re-lock stale -wal/-shm/-journal sidecars to 0o600 at startup. The
main file is chmod'd first, so any sidecar SQLite creates afterward
inherits 0o600, but a -wal/-shm left world-readable by an older 0o644
install (once WAL was enabled) could still expose DB pages. Absent
sidecars are the normal case, not an error.

Tests: unit-test _sqlite_db_path across driver/query/memory/postgres URL
forms, and a subprocess test asserting stale 0o644 -wal/-shm are
re-locked on startup.

* fix(db): handle sqlite file URI app db permissions

* fix(db): close remaining SQLite permission bypasses

---------

Co-authored-by: Ethan <23321960+0xLeathery@users.noreply.github.com>
Co-authored-by: Alexandre Teixeira <alexandremagteixeira@gmail.com>
chat_stream() references `_explicit_web_intent` in three places
(disabled-tools gating, global-disabled web allowance, and the
per-turn tool filter) but the assignment was dropped during a
branch merge. Every chat request raised

    NameError: name '_explicit_web_intent' is not defined

at routes/chat_routes.py, surfacing to the client as a bare
"Internal Server Error" before any LLM call was made — chat was
fully broken on dev and main.

Restore the original definition, computed from the already-derived
tool intent, immediately before its first use:

    _explicit_web_intent = bool(_tool_intent and _tool_intent.category == "web")

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2d81770)
…mporter (#5261)

* Harden skill importer against SSRF: block private targets + revalidate redirects per hop

The skill importer validated only the initial URL with the lenient SSRF guard
(block_private=False) and then fetched with follow_redirects=True, so a 3xx to
an internal/metadata address (169.254.169.254, 127.0.0.1, RFC-1918) was still
connected to — inconsistent with the hardened services/search/content.py
:_get_public_url path.

Add a _get_checked() helper that follows redirects manually and re-runs the
SSRF guard with block_private=True on every hop, and route all three fetch
sites (skills.sh unwrap, _fetch_bytes, _list_github_dir) through it. GitHub's
own redirects and the final-host _assert_github_url checks are preserved.

Adds hermetic regression tests (IP-literal hosts, faked HTTP layer) and updates
the existing mock signature for the new block_private kwarg.

Defense-in-depth: the endpoint is admin-gated (require_admin) and admins are
trusted per THREAT_MODEL.md, so this is not a cross-boundary vulnerability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: enforce follow_redirects=False invariant in mock client

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add Arch-specific package installation and NVIDIA runtime configuration
to the Docker setup guide. Cover passthrough verification, the NVIDIA
Compose overlay, and the distinction between GPU passthrough and
CUDA-backed model serving

Refs #831
chore: sync upstream changes from odysseus-dev/odysseus
fix(docker): bump Docker CLI to a patched release
fix(docker): bump Docker CLI to a patched release
* feat(models): define capability schema and readers

* fix(models): harden Google catalog probing

Restrict native catalog probing to the Gemini host, keep provider keys out of request URLs, filter non-chat model resources, and preserve the manual refresh default in the built-in Google add flow.
…(#5474)

* security(url-safety): reject RFC 6598 shared address space in strict mode

Strict mode (block_private=True) is a full SSRF lockdown, but it only
rejected is_private and is_loopback targets. CPython does not classify RFC
6598 shared/CGNAT space (100.64.0.0/10) as is_private (it is "shared", not
"private"), so a public redirect into 100.64.0.1 passed the per-hop guard
and still issued the request to a potentially internal CGNAT service.

not is_global would also exclude it, but only on CPython 3.11.10+/3.12.4+/
3.13+; the CI matrix runs 3.11/3.12, so reject the range explicitly to stay
correct across patch levels and the 3.14 runtime image. Default local-first
mode is unchanged. Adds strict-mode coverage for shared, non-global, and
public targets.

* docs(url-safety): correct CGNAT is_global rationale in strict-mode comment

The prior comment claimed `not is_global` catches 100.64.0.0/10 only on
CPython 3.11.10+/3.12.4+/3.13+. That is inaccurate for CGNAT: is_global
is False for 100.64.0.1 on every supported version (verified 3.10-3.14).
The version-fragility applies to other ranges gh-113171 touched, not CGNAT.
The explicit range reject is still the right choice; restate the reason as
is_private not covering shared space, and not coupling strict mode to
is_global's broader, cross-version definition. No behavior change.
…… (#5491)

* fix(llm): enhance fallback logic to handle empty completions and improve metadata handling

* fix(llm): stream tool call deltas immediately
Slice 2f of the route-domain reorganization (#4082/#4071, per
specs/architecture-runtime-inventory.md §6.3). Moves note_routes.py into
routes/note/, leaving a backward-compat sys.modules shim at the old path.
Pure file reorganization, no behavior change.

The shim uses sys.modules replacement (same pattern as the merged gallery
#4903, research #4975, memory #5007, history #5090, and contacts #5227
slices) so that `import routes.note_routes`, `from routes.note_routes import
X`, `importlib.import_module(...)`, and the `import ... as note_routes` +
`monkeypatch.setattr(note_routes, "SessionLocal", ...)` pattern used by
test_note_reminder_fire_scope.py / test_notes_fail_closed_auth.py all
operate on the same module object the application uses.

The canonical module does NOT depend on the shim — routes/note/note_routes.py
imports only from core/, src/, and stdlib. The outbound email cross-domain
imports (routes.email_routes._get_email_config, routes.email_helpers.
_send_smtp_message) are function-local lazy imports that keep resolving
through the email module's own path (email is not yet migrated).

One source-introspection test site repointed to the new canonical path:
- test_model_helper_owner_scope.py (shared with history; history entry
  already repointed in #5090, note entry repointed here)

Adds tests/test_note_routes_shim.py to pin the sys.modules shim contract
(legacy and canonical paths resolve to the same module object; monkeypatch
via legacy alias reaches the canonical module).

Verified: compileall clean; full suite 4487 passed, 3 skipped.
Pip installs and serve engines need a working Python environment on the
target, and on PEP 668 distros (Arch, newer Debian) — or hosts whose
python ships without pip — the practical answer is always "make a venv
and point the server profile at it". That was a manual, undocumented
dance: ssh in, python3 -m venv, come back, set env/path by hand.

New Venv button on each remote server entry (Settings → Servers, next
to Check/Key) calls POST /api/cookbook/setup-venv, which runs
`python3 -m venv <path>` on the target (default ~/odysseus-venv, or the
profile's existing venv path), verifies pip works inside it, and on
success the UI sets env=venv + the path on the profile automatically.

- Path validated against the same safe charset the shell routes use for
  venv paths, interpolated unquoted so a leading ~ expands remotely.
- Remote command runs via `sh -lc` so it survives non-POSIX login
  shells (fish/csh).
- Debian/Ubuntu's stripped ensurepip failure is detected and the error
  names the fix (apt install python3-venv).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 22, 2026
@catagris

Copy link
Copy Markdown
Author
image

William and others added 3 commits July 22, 2026 19:26
…ware venv setup

The venv is the foundation every pip dependency installs into, but it
lived only as a button in Settings — and it blindly used system
python3, which on Arch is 3.14: outside the serving stack's supported
window (numba and friends guard >=3.10,<3.14 at build time, not in
metadata pip could act on, so installs burned minutes before failing).

- setup-venv now selects the newest interpreter inside
  SERVING_PYTHON_RANGE (python3.13 → python3.10, then bare python3 if
  in range), falls back to `uv venv --seed --python 3.13` (uv fetches
  a standalone build, no sudo), and otherwise returns an actionable
  error naming the system python found and what to install. Rebuilds
  replace an existing venv only when pyvenv.cfg proves it is one.
- setup-venv {check:true} reports venv presence, its interpreter
  version, range compliance, and the creation candidate — powering a
  "Python environment" row pinned above the Dependencies package
  sections (remote servers): Ready / Fix (out-of-range rebuild) /
  Install states, with the profile wired automatically on success or
  when an existing healthy venv is detected.
- Settings Venv button reports the interpreter version it used.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both surfaced repeatedly while setting up a fresh serving server and
Cookbook could name them but not install them:

- uv is the Python-environment row's own fallback (fetches a supported
  standalone interpreter when the system Python is out of range) —
  recommending it while refusing to install it was half a feature.
- cargo (Rust toolchain) is a build prerequisite in the same class as
  cmake/gcc: pip deps like outlines_core fall back to sdist builds
  that need it.

Catalog rows (canonical name = probed binary, so the Rust row is
"cargo"), _PKG_NAMES per-OS mapping (Arch/brew ship the toolchain as
`rust`), install-system-deps allowlist + pacman/brew remaps, and the
frontend installable set. The Python-environment row's no-interpreter
state now points at the uv row below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The passwordless-sudo failure path tells users to "run the shown
install command once" — but the per-target command box only rendered
for build-prereq rows, so standalone System rows (tmux/uv/cargo)
failed with instructions pointing at a command that was shown nowhere.

The packages endpoint now attaches install_cmd_for_target (built from
the probed target OS and _PKG_NAMES) to uninstalled System rows, the
row renders the same command box build-prereq rows get, and the
existing failure toast automatically picks it up with its Copy-command
action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cookbook: one-click venv creation on server profiles (Venv button next to Check/Key)