Adversarial-review follow-ups to the lingo measurement() field:
- Add a `positive` option (strict > 0) and use it for the non-zero dimension
params. Swapping z.number().positive() → measurement(..,{min:0}) had started
admitting 0 (the core node schemas have no positivity backstop), so a zero-size
wall/opening/roof could be created. Inclusive-0 fields (overhang, sill height,
knee-wall height, opening offset, roof pitch) keep min:0.
- Escalate AMBIGUOUS_NUMBER to error so "1,234" fails instead of silently reading
as 1234 — a 1000x hazard for European decimals. Matches lingo's own /ai fields.
- roofLevelElevation reverted to z.number() (it's a level ordinal, not meters);
radians fields now advertise radian-appropriate examples.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measurement/angle parameters on the scene tools now accept a bare number OR a
natural-language string ("6 in", "180cm", "45°", "1.57rad"), canonicalized to
the unit the handler already expects (meters for length, radians/degrees for
angles) with min/max bounds and model-readable errors. Makes LLM tool calls
safer — "6 in" no longer has to be pre-converted to 0.1524, and a bad value is
rejected with a message the model can self-correct from.
A shared `measurement(kind, unit)` zod field wraps lingo's parseQuantity as a
`number | string` union+transform; numbers pass through unchanged (backward
compatible), so no handler changes were needed. Covers create_wall, cut_opening,
place_item, create_level, create_story_shell, create_roof,
create_stair_between_levels, create_room, add_door, add_window, photo_to_scene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Loading JSON previously called setScene blindly and crashed when
the file held schema-invalid nodes (e.g. items missing `asset`).
The new dialog parses the file, runs validateBuildJson in core,
and surfaces structure counts (site/building/levels/walls/doors/
windows/items/slabs/ceilings/zones/scans), floor area, and a
per-node Schema Details list grouped by type. Import is blocked
when any hard error exists.
Two schema bugs the validator surfaced are fixed here too:
- SiteNode.children is now an id array like every other node
(was a discriminatedUnion of full objects; three readers carried
a string-or-object ternary that's now dropped). migrateNodes
flattens legacy nested-object children on load. Default-scene
seed and photo-to-scene MCP builder updated to pass `building.id`.
- LevelNode.children now includes shelf — the editor allowed it
but the schema didn't. The schema-vs-registry-as-source-of-truth
discussion is captured in plans/editor-node-registry.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-push audit agent A2 flagged two HIGH-severity security issues that
would have shipped in the PR had we not checked:
1. PUT /api/scenes/[id] URL-validation bypass. Phase 8 P4 fixed
the POST /api/scenes route by replacing a loose z.unknown() graph
schema with AnyNode superRefine. The fix never made it to the PUT
handler - an attacker could resubmit the same javascript:/file:///
payloads via PUT. Fixed by extracting the tight validator into
apps/editor/lib/graph-schema.ts and sharing it across both routes.
2. SSRF in photo_to_scene / analyze_floorplan_image /
analyze_room_photo. All three tools called raw fetch(image) on
user-supplied URLs with no validation - a direct
http://169.254.169.254/latest/meta-data/ exfil primitive on any
cloud host. Added packages/mcp/src/lib/safe-fetch.ts that:
- Blocks loopback (127.0.0.0/8, ::1)
- Blocks link-local incl. cloud metadata (169.254.0.0/16)
- Blocks private ranges (10/8, 172.16/12, 192.168/16, fc00::/7)
- Blocks .local/.internal/.corp hostnames + localhost variants
- Blocks v4-mapped IPv6 loopback (::ffff:127.0.0.1)
- Manual redirects (max 3), revalidating the allowlist per hop
- 20 MB response-size cap (streamed, enforced per-chunk)
- 10s timeout
- Optional PASCAL_ALLOWED_ASSET_ORIGINS env allowlist
Tests: 8 new SSRF guard tests, all vision tests still pass, full
suite 302/302.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>