Two lines still cited the pre-A2 state (294 tests / 40 files,
"known gaps remain at save_scene / POST boundary"). Corrected
against the actual 302/41 and the Phase 10 A2 closure of those
boundaries via the shared apiGraphSchema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A5 wrote the initial PR description before the A2 security fixes
landed, so it cited 294 tests / 9 commits and listed several now-fixed
items as follow-ups. Updated:
- 294 -> 302 tests (8 new SSRF guard tests in safe-fetch.test.ts)
- 40 -> 41 test files
- 9 -> 20 commits with the actual Phase 5-10 series listed
- Security-notes section: document safeFetch + shared apiGraphSchema
wiring on both POST and PUT; remove "tracked follow-up" for
save_scene URL validation (done); add "tracked follow-up" keeps
same-id write race + auth layer
- Checklist: flip two items from [ ] to [x] for URL validation and
SSRF protection
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five parallel audit agents reviewed the branch before open-sourcing
the PR to pascalorg/editor:
- a1-secrets.md: SAFE TO PUSH. Scanned 176 files / 40,768 diff lines.
Zero secrets, tokens, API keys, PEM blocks, JWTs, or cookies.
Only MEDIUM finding: absolute /Users/adrian paths in test-report
scripts (cosmetic, not security).
- a2-security.md: FOUND 2 HIGH-severity issues, both FIXED in
commit 8757de0:
* PUT /api/scenes/[id] still had the loose graphSchema that POST
got fixed in Phase 8 P4. Shared schema extracted to
apps/editor/lib/graph-schema.ts so both routes re-validate.
* photo_to_scene + analyze_floorplan_image + analyze_room_photo
all did raw fetch(url) on user-supplied URLs - a textbook SSRF
to 169.254.169.254 cloud metadata. Added safe-fetch.ts with
private-IP / link-local / .local-hostname denylists, manual
redirect revalidation, size cap, timeout, env-allowlist.
- a3-code-quality.md: READY FOR REVIEW. Zero production `any`, all
tools Zod-validated in+out, uniform error handling,
conventional-commits. Two non-blocking follow-ups: client editor
components (SceneLoader, SaveButton) have no tests; document
check_collisions n^2 scaling.
- a4-performance.md: SHIP WITH NOTES. MCP dist 904 KB, Supabase
lazy-imported (zero editor bundle impact), v0.1 hot paths
sub-200ms. Flagged: FilesystemSceneStore.index.json O(n) per
write (fine <1k scenes), concurrency races (documented in P8),
client render at 5k nodes unverified.
- a5-pr-description.md: polished final PR description that
corrected stale test counts (294 not 142), disclosed all 5
cross-cutting surfaces, named the known failures honestly,
split the checklist, expanded the scope to the real Phase 7
deliverables.
Overall verdict: READY TO PUSH after the A2 fixes landed. No
blockers remain.
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>
Seeds the shared scene store with Casa del Sol + 3 templates + 3 Villa
Azul variants so /scenes can be browsed end-to-end after a fresh
`bun dev` + `pascal-mcp --http` restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds a larger, richer house than Casa del Sol via MCP save_scene
(no injection hack), then dispatches 10 parallel verifiers across
schema, geometry, dimensions, openings, HTTP, page render,
parentage, round-trip, spatial, visual.
Villa Azul — 56 nodes, validate_scene=true, 44KB on disk:
- 15x10m building envelope (vs Casa del Sol's 12x8)
- 9 interior zones (master bed/bath, bed 2/3, shared bath,
living/dining, kitchen, entry hall, corridor)
- 10 doors + 12 windows (all cut successfully)
- 4 exterior zones (pool 8x4 + basin slab at -2m, outdoor kitchen,
driveway, back patio)
- 5 rail-style fences (vs Casa del Sol's privacy) with 2m entrance gap
Verification: 108 checks, 104 PASS, 4 findings:
- V1 schema: 56/56
- V2 geometry: 7/7 (perimeter closes, interior T-junctions, no
zone overlaps, fence gap verified)
- V3 dimensions: 13/13 zone areas exact (1 spec mismatch on site
polygon default, not a build bug)
- V4 openings: 22/22 dimensional fit, surfaced a tool gap in
cut_opening (no adjacency check) + my build packed too tightly
- V5 HTTP: 10/10 (GET/PUT/PATCH/DELETE/HEAD, If-Match conflicts)
- V6 page: 14/14 (/scene/:id 81KB, /scenes 20KB, 404 fallback)
- V7 parentage: surfaced CROSS_CUTTING §2 site->building->level
parentId=null (pre-existing in core's loadScene)
- V8 round-trip: 10/10 byte-equal, duplicate_level -> 110 nodes
- V9 spatial: 12/12 (find_nodes, measure, constraints resource)
- V10 visual: HTML fallback (Chrome extension disconnected during
run); API layer intact
Follow-up tracked: `cut_opening` should check opening-adjacency on
the same wall (minimum gap) to catch tight packing during patch
construction. Currently returns success and relies on the UI to
visualise the overlap.
Live at http://localhost:3002/scene/a6e7919eacbe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8 parallel validation flagged two boundaries where malicious URLs
(javascript:, file:, external http:, data:text/html, ...) could be
persisted despite the AssetUrl allowlist added in Phase 7 A7:
1. `save_scene({ includeCurrentScene: false, graph })` — the graph arg
was treated as opaque (`z.record(z.string(), z.unknown())`) and
written to the store without re-running AnyNode.safeParse.
2. `POST /api/scenes { graph }` in the editor API — same issue; the
Zod `graphSchema` accepted anything object-shaped.
Fixes:
- `save-scene.ts`: when `includeCurrentScene === false`, iterate every
node and run `AnyNode.safeParse`; collect issues and throw
`McpError(InvalidParams, 'graph_invalid', { errors })` on any
failure.
- `app/api/scenes/route.ts`: replace `graphSchema` with a structured
`z.object({ nodes, rootNodeIds, collections? })` + `superRefine`
that runs `AnyNode.safeParse` on every node. Invalid → 400 with
detailed issue paths.
Tests:
- Added `save_scene` regression test for the P4 attack
(item.asset.src = 'javascript:alert(1)') — expected error.
- Fixed the existing `includeCurrentScene=false` test to use a
schema-compliant site node id (the prior `id: 'root'` now fails
the AnyNode parse, which is the desired strict behaviour).
- Full suite: 294 pass / 0 fail.
Also adds Phase 8 test-reports/phase8/** (10 agents, ~15 scripts +
markdown reports) documenting the validation run, plus minor biome
cleanups to the Phase 5/7 test artefacts (removed stale
`// biome-ignore` suppression comments that now resolve to the
already-off `noConsole` rule).
Phase 8 result summary (10 parallel agents, stdio MCP transport with
isolated data dirs):
- P1 templates: 18/18 PASS
- P2 variants: 6/7 mutations + determinism + save + combined + error
- P3 locking: 12/12 PASS (MCP + editor HTTP If-Match)
- P4 URL hardening: fixed 2 bypasses (see above)
- P5 photo-to-scene: 6/6 PASS
- P6 Casa del Sol via save_scene: 13/13 PASS
- P7 editor HTTP API: 18/18 PASS
- P8 concurrency: 4/5 PASS, flagged 2 real filesystem-store races
(expectedVersion CAS gap + .index.json drift under parallel writes)
- P9 edge cases: 13/13 PASS (size cap, slug safety, bad inputs)
- P10 full sweep: 37/37 PASS (30 tools + 4 resources + 3 prompts)
Known follow-ups:
- FilesystemSceneStore needs a proper lockfile / atomic CAS to fix
the P8 concurrency bugs (low priority: single-writer MCP is the
typical case).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 parallel research agents (R1-R10) investigated whether MCP can save
scenes as saveable entities that the user later opens in the editor —
rather than injecting via a dev-only window.__pascalScene hook.
Key findings:
- Editor is already backend-agnostic via onLoad/onSave callbacks (R2,R7)
- Current persistence is localStorage-only, single key (R1)
- Zero dynamic routes and no backend code yet — env declared (R4,R5)
- File import exists as "Load Build" but lacks Zod validation (R6)
- MCP-written scenes load cleanly into the editor today (R6, Casa del
Sol test already proved this)
- Best path: filesystem-handoff this week, Supabase in weeks 2-4,
Supabase Realtime for live mode in Q2. Skip Yjs for now. (R8)
- 4-5 weeks to private beta, 10-14 to GA (R9)
- 10 high-value ideas ranked; "photo -> scene" is the unblocker (R10)
SYNTHESIS.md pulls all 10 reports together with the recommended
implementation plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design and live-built a 3-bed 2-bath single-story house with pool and
privacy-screened perimeter ("Casa del Sol") using only @pascal-app/mcp
tools. Exercises every mutation tool in a realistic scenario.
- DESIGN.md: blueprint (lot 20x15, building 12x8, 7 interior zones,
pool 5x3, 5 privacy fence segments)
- build.ts: MCP client orchestrator
- scene.json: full exported scene (26.7 KB)
- BUILD_REPORT.md: per-step status + final totals
Final scene: 39 nodes pre-duplicate (1 site, 1 building, 1 level,
9 walls, 9 zones, 6 doors, 6 windows, 5 fences, 1 slab), 76 nodes
post-duplicate_level, valid=true, errors=0.
Surfaced the documented StreamableHTTP single-session limitation:
the HTTP server at :3917 rejected the SDK client's initialize with
"Server already initialized" because the previous T3 session consumed
the transport's one-shot session slot. Build fell back to an in-memory
transport exercising the identical tool surface. Fix tracked for v0.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SceneBridge.applyPatch now threads the Zod-parsed node through to the
apply phase instead of the raw input. Previously, when a caller sent a
create patch without an `id` field, the schema's objectId default ran
during dry-run parsing but only in `res.data`; the apply phase pushed
the unparsed `p.node` (no id) to the store, so subsequent tools that
walked `level.children` crashed on undefined entries (e.g.
duplicate_level -> cloneLevelSubtree -> extractIdPrefix(undefined)).
Also adds test-reports/ artefacts from live end-to-end testing:
- t1-stdio: 21/21 tools pass via stdio (~106ms)
- t2-http: connect/single-session behaviour (HTTP transport quirk
documented)
- t3-scenario: 2-bedroom apartment built end-to-end — 12/12 steps
after this fix (24 final nodes, validate=true, apartment.json
exported)
- t4-errors: 24/24 invalid-input cases rejected with proper MCP errors
- t5-resources-prompts: 4/4 resources, 3/3 prompts, dev server
/api/health 200 OK
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full PR description summarising the 9-commit series, tool/resource/prompt
inventory, architecture, verification checklist, and follow-ups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .github/workflows/mcp-ci.yml: runs on pushes to main and PRs
touching packages/mcp/, packages/core/, or bun.lock. Installs with
Bun 1.3.0, builds core then mcp, runs bun test, and runs biome.
- packages/mcp/CROSS_CUTTING.md: documents the two additive changes
outside the package boundary (core exports map, this CI workflow)
plus observations about SiteNode.children inconsistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README.md: install/quick start; configs for Claude Desktop, Claude
Code, and Cursor; programmatic usage; tables covering all 21 tools,
4 resources, and 3 prompts; limitations; development commands.
- CHANGELOG.md: 0.1.0 entry in Keep a Changelog format.
- examples/generate-apartment.md: prose transcript using from_brief
to build an 80 m² 2-bed apartment, showing apply_patch, set_zone,
cut_opening, validate_scene.
- examples/renovate-from-photos.md: prose transcript using the vision
tools + renovation_from_photos prompt.
- examples/embed-in-agent.ts: compilable TypeScript showing
programmatic usage via InMemoryTransport.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- connectStdio(server): wires the MCP server to StdioServerTransport.
- connectHttp(server, port): wires the MCP server to
StreamableHTTPServerTransport over node:http. Returns a handle with
port and close().
- bin/pascal-mcp.ts: CLI entrypoint with --stdio (default), --http,
--port, --scene, --help, --version. Loads node-shims before any
core import. Logs to stderr (stdio transport uses stdout for
JSON-RPC).
- scripts/smoke.ts: end-to-end smoke — spawns the stdio bin, connects
as an MCP Client, exercises get_scene, create_level, validate_scene,
undo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
analyze_floorplan_image and analyze_room_photo defer the vision work
to the host via MCP sampling (server.server.createMessage). Validates
host capability before calling, fetches URL inputs and base64-encodes
them, constrains output to a Zod schema, and returns structured
content. No vision model is bundled.
9 tests, all passing via a mocked sampling-capable client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resources: pascal://scene/current (JSON), /scene/current/summary
(markdown with per-level counts, floor areas, bbox), /catalog/items
(returns catalog_unavailable in headless mode), and the templated
pascal://constraints/{levelId} which exposes slabs + wall footprints
via @pascal-app/core/wall helpers.
Prompts: from_brief (generate scene from a natural-language brief),
iterate_on_feedback (minimal-diff patch proposals), and
renovation_from_photos (orchestrates the vision tools).
17 tests, all passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SceneBridge class wraps @pascal-app/core's Zustand store for Node,
exposing a clean programmatic API for scene load/mutate/export plus
Zundo undo/redo. Requires a requestAnimationFrame polyfill loaded
before any core import to work around the store's RAF-batched dirty
marking.
- 51 tests, 99.68% line coverage on scene-bridge.ts
- All-or-nothing applyPatch with Zod dry-run validation
- Safeguards against prototype-polluting keys in loadJSON
- Resolves children through the flat nodes dict (handles the
SiteNode.children-as-objects inconsistency)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds packages/mcp/.gitignore, src/index.ts (public API barrel),
src/server.ts (createPascalMcpServer factory), and a smoke test
for the public API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add packages/mcp/ with package.json, tsconfig, PLAN.md, CROSS_CUTTING.md
- Extend @pascal-app/core exports map with subpaths (./schema, ./store,
./material-library, ./spatial-grid, ./wall) so Node consumers can skip
the graphics-bound systems re-export chain.
- Add scripts/spike.ts that proves useScene + temporal work in Node with
a requestAnimationFrame polyfill. Spike passes end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Guard _buildCache in merged-outline-node against stale/disposed
Object3D refs that cause TypeError on .id access during render
- Reset children array when duplicating roofs to prevent inconsistent
parent-child relationships (matching existing stair behavior)
- Use obj?.parent check in EditorOutlinerSync to ensure objects are
still in the scene graph before adding to outliner arrays
- Resume temporal state on parse failure to prevent undo/redo freeze
Closes#232
The WebGPURenderer needs its backend initialized before any direct
`.render(scene, camera)` call. The commented-out `// renderer.init()`
said "Only use when using <DebugRenderer />", but the non-debug path
also calls direct render from the post-processing fallback
(post-processing.tsx:318), which throws "Renderer: .render() called
before the backend is initialized" on any browser that falls back to
the WebGL2 backend.
Switching the gl factory to an async function and awaiting init()
before returning is safe in both backends (init() is idempotent and
the async factory is a supported @react-three/fiber v9+ pattern), and
prevents the error in WebGL2 fallback.