Blender add-on: importGltf, setCamera, setLight, setWorld, renderStill #2

Merged
libby merged 6 commits from libby/blender-scene-actions into main 2026-11-07 15:13:27 +00:00
Member

Adds five Blender command actions (brokkr.blender.command_intent.v0) to surfaces/blender/brokkr_bridge. Separate branch off main, independent of the server-hardening PR (#1).

action fields rule
importGltf filepath .gltf/.glb inside the Import Directory preference only
setCamera targetObjectName, lens, cameraType (PERSP/ORTHO/PANO), panoramaType (e.g. EQUIRECTANGULAR), orthoScale, clipStart, clipEnd, active target must be a camera; active sets scene.camera
setLight targetObjectName, energy, color [r,g,b], lightType target must be a light; energy ≥ 0; colour 0..1
setWorld worldName, color, strength edits the World's Background node; creates/assigns a world if none
renderStill outputPath, engine, resolutionX/Y, cameraName output only inside the Render Output Directory preference; .png/.jpg/.exr; engine must be one the running Blender offers; 1..16384 px

Safety rules

  • Both directories are add-on preferences, default empty = action disabled. A candidate path is joined to the root, then resolved (.. collapsed, symlinks followed), then tested for containment: ../x, absolute paths and symlinks that escape are refused before any bpy call.
  • All fields are validated (finite numbers, ranges, enums, colour triples) before anything is changed, so a refused intent leaves the scene untouched and gets a failed receipt naming the rule.
  • renderStill restores engine, resolution, camera, file path and format afterwards, including when the render raises. It blocks the UI while rendering (documented).
  • Capabilities asset.import.gltf, camera.write, light.write, world.write, render.still added to the add-on's list. The Rust daemon's brokkr.blender_editor advertisement is not updated here (left for a follow-up so this PR stays in the add-on and docs).

Tests

python3 -m unittest discover -s surfaces/blender/tests -v: 16 tests with a fake bpy, all pass: containment (including .., absolute, symlink escape), disabled-when-unconfigured, extension/exists checks, camera/light/world field effects and whole-intent refusal, panorama property fallback for Blender < 4.2, render restore on success and on failure, nothing renders on any refused input.

Not tested (Blender not installed or run)

The real bpy.ops.import_scene.gltf, render ops, panorama_type location across Blender versions, world node names (Background, inputs Color/Strength), engine enum names in the target Blender. Needs a live Blender smoke.

Merge note

Touches __init__.py (new preferences, configure_action_dirs calls before drain) in places the server-hardening PR (#1) also edits nearby; expect a trivial textual conflict to resolve for whichever merges second. Not merged.

🤖 Generated with Claude Code

Adds five Blender command actions (`brokkr.blender.command_intent.v0`) to `surfaces/blender/brokkr_bridge`. Separate branch off `main`, independent of the server-hardening PR (#1). | action | fields | rule | |---|---|---| | `importGltf` | `filepath` | `.gltf`/`.glb` inside the **Import Directory** preference only | | `setCamera` | `targetObjectName`, `lens`, `cameraType` (`PERSP`/`ORTHO`/`PANO`), `panoramaType` (e.g. `EQUIRECTANGULAR`), `orthoScale`, `clipStart`, `clipEnd`, `active` | target must be a camera; `active` sets `scene.camera` | | `setLight` | `targetObjectName`, `energy`, `color` [r,g,b], `lightType` | target must be a light; energy ≥ 0; colour 0..1 | | `setWorld` | `worldName`, `color`, `strength` | edits the World's Background node; creates/assigns a world if none | | `renderStill` | `outputPath`, `engine`, `resolutionX/Y`, `cameraName` | output only inside the **Render Output Directory** preference; `.png/.jpg/.exr`; engine must be one the running Blender offers; 1..16384 px | ## Safety rules - Both directories are add-on preferences, **default empty = action disabled**. A candidate path is joined to the root, then resolved (`..` collapsed, symlinks followed), then tested for containment: `../x`, absolute paths and symlinks that escape are refused before any `bpy` call. - All fields are validated (finite numbers, ranges, enums, colour triples) **before** anything is changed, so a refused intent leaves the scene untouched and gets a `failed` receipt naming the rule. - `renderStill` restores engine, resolution, camera, file path and format afterwards, including when the render raises. It blocks the UI while rendering (documented). - Capabilities `asset.import.gltf`, `camera.write`, `light.write`, `world.write`, `render.still` added to the add-on's list. The Rust daemon's `brokkr.blender_editor` advertisement is **not** updated here (left for a follow-up so this PR stays in the add-on and docs). ## Tests `python3 -m unittest discover -s surfaces/blender/tests -v`: 16 tests with a fake `bpy`, all pass: containment (including `..`, absolute, symlink escape), disabled-when-unconfigured, extension/exists checks, camera/light/world field effects and whole-intent refusal, panorama property fallback for Blender < 4.2, render restore on success and on failure, nothing renders on any refused input. ## Not tested (Blender not installed or run) The real `bpy.ops.import_scene.gltf`, render ops, `panorama_type` location across Blender versions, world node names (`Background`, inputs `Color`/`Strength`), engine enum names in the target Blender. Needs a live Blender smoke. ## Merge note Touches `__init__.py` (new preferences, `configure_action_dirs` calls before drain) in places the server-hardening PR (#1) also edits nearby; expect a trivial textual conflict to resolve for whichever merges second. Not merged. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Paths for importGltf and renderStill are confined to the Import Directory and
Render Output Directory preferences (empty disables the action; .. , absolute
paths and escaping symlinks are refused). Every field is validated before bpy is
touched. renderStill restores render settings afterwards. Tests use a fake bpy;
Blender was not run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolved blender_target.py: both the server-guard fields and the action directories kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found by running the actions on Blender 5.2.3 headless: the render engine enum is
dynamic, bl_rna lists only BLENDER_EEVEE, so CYCLES and WORKBENCH were refused.
The fake in the tests now behaves like the real enum. surfaces/blender/tests/live/
headless_smoke.py runs every scene action against real bpy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
WIP for review fixes, tests pass (61); mutation confirmation and Starfire re-run pending.

- importGltf parses .gltf JSON and .glb's JSON chunk, checks every buffers/images uri.
- Names are relative plain paths: no drive, UNC, extended, ':' (ADS), '..', device names.
- No symlink/junction component under the root; output must not already exist.
- Path and file stamps re-checked right before use; residual TOCTOU documented in code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- resolve_inside maps OSError from resolving a component (WinError 448 on a mklink /J junction) to a link refusal.
- Direct check_relative test for '..' and NUL; OSError mapping test.
- headless_smoke.py makes real junctions on Windows (outside and inside the root) and an existing-output case.
- install.md documents the path rules and the residual TOCTOU.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- check_relative: spaces before the first dot are dropped (CON .txt), superscript COM/LPT digits are device names.
- gltf_references refuses a reference to a missing file; drops the broad ValueError catch and redundant relative_to so the specific refusal (a link) reaches the receipt.
- _render_still makes folders only after every Blender-side refusal (unknown engine used to leave them behind).
- Tests for each.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
libby merged commit 82ed1e73cb into main 2026-11-07 15:13:27 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Brokkr!2
No description provided.