Files
esh-pfi-infrastructure/stacks/blender/README.md
T
vh 83dc497b40 feat(blender): pinned extension set in a read-only System repo, for the GUI and blender-run --extensions
Blender is now a mandatory stage in draupnir's pipeline (Prime, 2026-09-28), and draupnir asked
for eight add-ons from extensions.blender.org: SurfacePsycho 0.10.4, CAD Sketcher 0.32.1,
3D-Print Toolbox 1.4.1, STEP Importer 1.2.1, Bool Tool 2.1.0, LoopTools 4.7.7, MeasureIt 1.8.4,
3MF Import/Export 2.7.7.

- stacks/blender/extensions.lock pins each by version and archive sha256.
- scripts/blender-extensions sync builds fv-ml1:/tank/blender-extensions/5.2/system with Blender's
  own install-file, pre-warms and byte-compiles it, checks a read-only enable, then swaps it in.
  It refuses while the GUI or a blender-run job holds the old directory.
- conf/scripts/startup/fleet_extensions.py enables every package in the System repo: in a timer
  in the GUI (after the prefs load), and as --python ahead of the caller's args in
  blender-run --extensions (a failed enable exits 1 before the caller's script).
- It also patches SurfacePsycho's sp_overwrite_segment_selection from eval() to literal_eval():
  the eval walked past MCP safe mode (control: unpatched ran code, patched refuses).
- blender-run: --extensions (bind mounts via --mount so a missing source fails instead of being
  created); USER/LOGNAME set, which CAD Sketcher's getpass needs.
- compose.yaml mounts the repo read-only and the hook into the GUI container. NOT yet deployed.
- scripts/blender-probes/extensions_acceptance.py: one operator run per add-on, safe-mode
  compliant. Headless 8/9 online and with --network none; CAD Sketcher sketching is GUI-only.
  A Python audit hook saw no network/process events (positive control fired).
2026-09-28 12:50:57 -07:00

228 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# blender
**Blender 5.2.2 LTS on fv-ml1 GPU 3, on demand.** Agents drive it; there is also a browser
desktop to watch it or take over. Prime, 2026-09-27: "go ahead with gpu 3, both". He does not
use Blender himself, so the agent side (MCP) is the primary interface.
| | |
|---|---|
| **Desktop** | `https://10.251.50.54:3001` (self-signed cert). Basic auth: user `blender`, password `secret get fv-ml1/blender-web-password`. |
| **Image** | `lscr.io/linuxserver/blender:5.2.2-ls241@sha256:9216c77a…` (Selkies 2.0 Wayland desktop, NVENC stream). |
| **GPU** | GPU 3 only (`NVIDIA_VISIBLE_DEVICES=3`). About 270 MiB is held while the desktop runs. |
| **Files** | `/work` → `/tank/blender` (projects, assets, renders; **not backed up**). `/config` → `/opt/docker/data/blender` (prefs, add-ons; restic). Files are owned by infra-ops (uid 1002), so agents can `scp` in and out. |
## ⚠ On demand: GPU 3 is borrowed
GPU 3 is the fleet's reserve card for a full-size vLLM seat (`servers/fv-ml1/README.md`).
Blender uses it only while in use:
```bash
ssh infra-ops@10.251.50.54 'cd /opt/docker/compose/blender && docker compose up -d' # start
ssh infra-ops@10.251.50.54 'cd /opt/docker/compose/blender && docker compose down' # stop, card back to 0
```
`restart: "no"`, so a reboot never brings it back. **When a big seat moves onto GPU 3, Blender
stays down.**
## Headless rendering: `scripts/blender-run` (for scripts and CLI callers)
A one-shot `docker run --rm` of this image with Blender as the entrypoint. It needs no desktop and
does not collide with the GUI container's up/down. `--job DIR` stages a local dir to
`/tank/blender/jobs/<name>/` and copies results back. Engines tested headless on 2026-09-28:
Cycles GPU and CPU, EEVEE (EGL), Workbench. STL import is built in. **`--extensions`** also
enables the pinned add-on set (STEP import and export among them; see "Extensions" below).
Foot-guns and budget are in `docs/fleettools/blender.md`. First consumer: draupnir.
## Headless rendering inside the running GUI container
```bash
ssh infra-ops@10.251.50.54 'docker exec -u abc blender blender -b /work/<file>.blend -E CYCLES -o /work/out/frame_#### -a -- --cycles-device OPTIX'
```
Run as `-u abc`, the image's user, mapped to uid 1002, so outputs land owned by infra-ops.
## Acceptance (2026-09-27, 1356)
- Cycles sees the card on both OptiX and CUDA: "NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation
Edition". The build ships `kernel_sm_120.cubin` plus OptiX PTX.
- Self-test (`/tank/blender/render_test.py`): a subdivided glass monkey, 1920×1080, 1024 samples,
32 bounces, no denoise. **OptiX 3.54 s against CPU 22.71 s (96 threads).** That is n=1 per
device: a functional check that the GPU is really used, not a benchmark. A trivial default-cube
scene could not separate them (0.59 s against 0.65 s), which is why the self-test scene is heavy.
- Web auth: no credentials gives 401, a wrong password 401, the right one 200.
- Selkies: "Render node 1 encodes H264, AV1, H265 on nvenc"; the Wayland renderer runs GL on GPU 3.
## Agent control (MCP): `scripts/blender-mcp`
**The chosen server is [mcp-for-blender](https://github.com/ahujasid/mcp-for-blender)** (MIT, one
maintainer, ~29k stars; researched by dvalin-smithy-dev 2026-09-27, thread
`01M3JA61FTFW2ZPSD20MHF7RPJ`, full note in dvalin-smithy
`research/blender-agent-drive-2026-09-27.md`). Two halves:
- **The add-on** is inside the running GUI Blender and serves a socket that executes arbitrary
Python with **no authentication**. It is vendored at upstream commit `41a18432`
(`conf/scripts/addons/blender_mcp.py`, MIT licence alongside) and started by
`conf/scripts/startup/fleet_mcp.py`. The add-on only serves from a GUI Blender, never from
`blender -b`, which is one reason the desktop exists.
- **The MCP server**: `mcp-for-blender==2.1.1`, frozen in `conf/mcp-requirements.txt` and installed in
`/work/.mcp-venv` **inside the container** (`scripts/blender-mcp setup`). Agents reach it as
stdio over `ssh infra-ops@fv-ml1 docker exec -i`. It runs in the container for two reasons:
1. **No port is published.** The socket stays on the container's localhost, so access means
ssh + docker on fv-ml1.
2. **Viewport screenshots need a shared filesystem.** Blender writes the image and the server
reads it back. With the server on nh3-dev it failed ("Screenshot file was not created").
Settings: `DISABLE_TELEMETRY=true` and `BLENDER_MCP_SAFE_MODE=1`. Safe mode puts upstream's AST
allowlist in front of `execute_blender_code`: bpy, bmesh, mathutils and pure stdlib only, and no
os, open, eval or network. It guards against prompt injection from third-party asset text; it is
not a sandbox.
### For an agent
```bash
scripts/blender-mcp up # GPU 3 is borrowed: start only when needed
scripts/blender-mcp status # wait for "mcp add-on: answering" (~10-40 s)
claude mcp add blender -- /home/lkraven/development/eshpfi-management/scripts/blender-mcp # PER TASK (Prime 2026-09-27): never user/project-wide
scripts/blender-mcp down # when finished: the card goes back to 0
```
- **Always pass `user_prompt`.** The tools require it; it is a short statement of the user's
request.
- **Render on the GPU:** set `scene.cycles.device = 'GPU'` on any scene you create. Safe mode
forbids touching `bpy.context.preferences`, so the startup hook has already pointed Cycles at
OptiX on GPU 3. The startup scene is already set to GPU.
- **Save everything under `/work/…`** (= `fv-ml1:/tank/blender`), then `scp` it out.
- **Look before you report:** `get_viewport_screenshot` returns an image, and a still render to
`/work` is the real check.
- The asset tools (Poly Haven etc.) reach the internet from Blender. The paid ones (Hyper3D,
Hunyuan, Tripo, Sketchfab) need keys we do not have; leave them off.
### Acceptance (2026-09-27, 1433)
An MCP client on nh3-dev → `scripts/blender-mcp` → the in-container server → the add-on:
- `initialize` OK; **36 tools** listed.
- `execute_blender_code` built a gold metallic torus and rendered it with Cycles on the GPU to
`/work/_selftest/mcp-torus.png`. The file landed, and I checked it by eye.
- `get_viewport_screenshot` returned an image of the scene.
- **Negative control:** `import os` was rejected by safe mode.
- `status` pings the add-on itself. A TCP probe was useless because the port accepted while
Blender was still loading.
⚠ Two traps found on the way, both fixed and commented where they live:
- Enabling the add-on from a startup script gets undone when user prefs load. The hook enables it
in a timer instead.
- A pre-flight `ssh` without `-n` swallowed the MCP client's `initialize`, and the session hung at
init.
## Extensions: the pinned add-on set (2026-09-28)
**Why:** Prime's ruling of 2026-09-28 makes Blender a mandatory stage in draupnir's pipeline
(requirements → functional shape in build123d → industrial design in Blender → print). draupnir
asked for these add-ons (thread `01M3MQEGGR0N981645WNZ9DMF3`). All come from extensions.blender.org,
all are GPL, and all are pinned by version and archive sha256 in [`extensions.lock`](extensions.lock).
| Add-on | Version | For | Wheels |
|---|---|---|---|
| SurfacePsycho | 0.10.4 | NURBS/Bezier patch surfacing; STEP/IGES export (the route back to build123d). Alpha. | cadquery-ocp-novtk 7.9.3.1 (cp313) |
| CAD Sketcher | 0.32.1 | Constraint-based precise profiles | slvs 3.2 (cp313) |
| 3D-Print Toolbox | 1.4.1 | Mesh cleanup (clean non-manifold) and checks before a mesh leaves Blender | none |
| STEP Importer (Clonephaze) | 1.2.1 | STEP in, from vendor parts and functional shapes | cascadio 0.0.18rc8 (abi3) |
| Bool Tool | 2.1.0 | Hard-surface booleans | none |
| LoopTools | 4.7.7 | Mesh helpers | none |
| MeasureIt | 1.8.4 | Dimensions drawn in the viewport (they show in screenshots) | none |
| 3MF Import/Export (Clonephaze) | 2.7.7 | 3MF, which carries units (STL does not) | none |
Deliberately skipped (draupnir): ND, HardOps, BoxCutter (modal only, an agent cannot drive them),
Quad Remesher (paid; the built-in QuadriFlow covers it), QRemeshify (not in the 5.2 catalogue).
### How it is wired
```
stacks/blender/extensions.lock ──scripts/blender-extensions sync──▶ fv-ml1:/tank/blender-extensions/5.2/system
│ mounted READ-ONLY as Blender's
│ System repo (/blender/5.2/extensions/system)
┌───────────────────────────────────────────┴──────────────────────────┐
GUI container (compose.yaml) blender-run --extensions
conf/scripts/startup/fleet_extensions.py enables all the same file runs as --python ahead of the
of it in a timer after the prefs load caller's args; any failure exits 1 first
```
- **Nobody installs from inside Blender.** MCP safe mode blocks `bpy.ops.extensions.*`,
`addon_enable` and `register_class`, and the repository is mounted read-only, so an agent can
neither add an add-on nor alter one. Everything in the System repo is enabled; that directory
holds exactly the lock.
- **Wheels** (OCP, slvs, cascadio) are unpacked by Blender into the USER extensions dir, about 60 MB.
In the GUI that is `/config` (= `/opt/docker/data/blender`, restic). In blender-run it is the
container's `/tmp`, rebuilt every run, so concurrent runs share nothing.
- **`scripts/blender-extensions sync`** downloads each archive (cached in
`/tank/blender-extensions/zips/`), checks its sha256, installs it with Blender's own
`--command extension install-file` into a staging dir, pre-warms and byte-compiles it, checks
it enables read-only, and only then swaps it in. It **refuses while the GUI container or a
blender-run job is running**, because they hold the old directory through the bind mount.
`scripts/blender-extensions status` compares what is installed with the lock.
- **Bumping a version:** edit the lock row, re-run the audit below on the new archive, stop the
GUI (`scripts/blender-mcp down`), `scripts/blender-extensions sync`, run the acceptance probe.
### Fleet-local fixes (in `fleet_extensions.py`, both paths)
- **SurfacePsycho `view3d.sp_overwrite_segment_selection` runs `eval()` on its string property.**
That walks straight past MCP safe mode: an agent's `bpy.ops` call passes the AST check, and the
string inside it is never parsed. Nothing in the add-on calls that operator, so the hook swaps
`eval` for `ast.literal_eval`, which keeps its documented use (a literal set of segment ids) and
refuses code. **Control (2026-09-28):** unpatched, the payload `[__import__('os').getpid()]` ran
and returned `[1]`; patched, it raised `ValueError: malformed node`; the literal `{3, 5}` worked
both ways. The hook logs a warning if the upstream code changes.
- **3MF's "please rate us" popup** (after five exports) is switched off.
### Phone-home audit (2026-09-28)
- **Manifests:** none of the eight declares the `network` permission (five declare `files` only).
- **Source:** no add-on imports `socket`, `urllib.request`, `requests`, `http`, `subprocess` or
`webbrowser` (3MF uses `urllib.parse` for path joining only). `wm.url_open` appears only behind
buttons a person clicks (CAD Sketcher's help links, 3MF's rating dialog), and safe mode blocks
`wm.url_open` in agent code anyway. The GUI hook logs `bpy.app.online_access`.
- **Runtime:** the full acceptance probe ran with `docker run --network none` under a Python
audit hook watching `socket.*`, `urllib.Request`, `http.client.*`, `subprocess.Popen`,
`os.system`/`exec`/`posix_spawn` and `webbrowser.open`. **Zero events**, and every operator
passed offline. **Positive control:** one deliberate `socket.getaddrinfo` in the same setup
showed up as an event. **Sensitivity floor:** the hook sees Python-level calls only. The native
wheels (OCCT, SolveSpace) could open a socket without Python seeing it, but nothing needed the
network to work, and none of them is a networking library.
### Foot-guns (all measured 2026-09-28)
- **`blender --addons x,y` is not the same as enabling.** It leaves the add-on out of
`preferences.addons`, and Bool Tool and LoopTools read their own prefs in `register()`, so
they failed with a KeyError. The hook enables them the way the Preferences button does.
- **3D-Print Toolbox writes a cache into its own package dir** on first import. On the read-only
mount that fails, which is why `sync` pre-warms the staging copy while it is still writable.
- **CAD Sketcher calls `getpass.getuser()`**, which fails for a uid with no passwd entry.
blender-run sets `USER`; the GUI's `abc` user exists.
- **CAD Sketcher's sketch operators need the GUI.** Creating a sketch activates a workspace tool,
and in `blender -b` there is none: `'NoneType' object has no attribute 'widget'`. The add-on
loads headless and its solver (`slvs`) imports; sketch authoring is a GUI/MCP job.
- **`Object.dimensions` is in local axes** and ignores rotation, so it cannot show which way an
import faces. The probe uses world-space bounds.
- **STEP Importer orientation:** a SurfacePsycho STEP (Z-up, mm) comes back the right way up with
the importer's default `up_axis="Y"`. `up_axis="Z"` stands it on its edge (this is the probe's
control for its own orientation check).
- **SurfacePsycho's default patch is 2 x 2 m** (Blender units), and STEP export scales ×1000 into
millimetres by default. Scale the object to part size first.
- **MeasureIt and LoopTools want a 3D View area.** In the GUI, override with the window's
VIEW_3D area; headless, a screen datablock's VIEW_3D area works (`view3d_override()` in the probe).
### Acceptance
The probe is [`scripts/blender-probes/extensions_acceptance.py`](../../scripts/blender-probes/extensions_acceptance.py):
one real operator run per add-on, written to pass MCP safe mode so the same code runs on both
paths. Files land in `fv-ml1:/tank/blender/acceptance/extensions/`.
**Headless, 2026-09-28:** 8 of 9 checks PASS, both online and with `--network none`: print3d
clean non-manifold (4 → 0 non-manifold edges), Bool Tool auto difference, LoopTools circle
(radius spread 4.14 mm → 0), MeasureIt segment, SurfacePsycho patch → STEP
(`sp-patch-headless.step`, 20 × 20 mm), STEP Importer read-back (20 × 20 × 0 mm world, plus the
wrong-axis control), 3MF out and back (20 mm cube kept its size). The one FAIL is CAD Sketcher,
for the GUI-only reason above. These runs used the same docker invocation blender-run makes.
**MCP (GUI) path:** pending the stack deploy that mounts the repo and hook into the GUI container.