Files
esh-pfi-infrastructure/stacks/blender/README.md
T

255 lines
17 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.
**Round trip into build123d (draupnir, 2026-09-28):** `sp-patch-headless.step` imported on
irv-ml1 with build123d 0.12.0 as one Shell holding one BSPLINE face, bbox ±10.000000 mm in X/Y at
z = 0, area 399.999982 mm² against 400. Extruded 2 mm, it gave a valid 799.999964 mm³ solid. So a
real NURBS surface reaches the kernel, not triangles, and millimetres hold across the hop. That is
one run of a flat bilinear patch; curved and trimmed patches are untested.
**MCP (GUI) path, 2026-09-28 1506 (after Prime ran the deploy):** the hook log read
`enabled 8/8`, `hardened: surfacepsycho ... literal_eval`, `online access: False`. Through
`scripts/blender-mcp` → `execute_blender_code` under safe mode, the probe passed **9 of 9**,
including CAD Sketcher (a sketch on the XY origin plane, then a full solve), because the GUI has
the workspace tool that `-b` lacks. The eval patch held under MCP: the code payload was refused
(`ValueError: malformed node`) and the literal `{3, 5}` was accepted.
- **CAD Sketcher sketch, re-solved headless:** the MCP-made sketch was saved
(`cad-sketch-mcp.blend`), then `blender-run --cpu --extensions` opened it and
`view3d.slvs_solve(all=True)` returned FINISHED. Authoring is in the GUI; re-solving works in batch.
- **Not solved yet:** drawing CAD Sketcher geometry from code. `slvs_add_rectangle` with
`p1_fallback`/`p2_fallback` and `wait_for_input=False` raised
`'NoneType' object has no attribute 'co'`. Its drawing ops are stateful and expect point picks,
and its Python entity API is out of reach under safe mode. Sketches work; agent-drawn profiles
need more work.
- **Not confirmed:** MeasureIt overlays in `get_viewport_screenshot`. The segment was created and
`measureit.runopengl` returned FINISHED, but two screenshots (a top view with a 20 mm edge
dimension) showed no dimension.
- `blender-run --extensions` (the real wrapper, `--cpu`) ran the same probe at 8 of 9, the one
miss being CAD Sketcher for the headless reason above.
Safe-mode note for probe authors: safe mode refuses calling a function held in a variable and
any dunder attribute (`type(ex).__name__`). The probe calls each test by name and uses `repr(ex)`.