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>
3.3 KiB
3.3 KiB
Phase 8 P10 — full sweep (stdio MCP)
Generated: 2026-04-19T18:21:20.973Z
Summary
- Transport: stdio (
bun packages/mcp/dist/bin/pascal-mcp.js --stdio) - Data dir:
/tmp/pascal-phase8-p10 - Sampling: mocked via
client.setRequestHandler(CreateMessageRequestSchema, …) - Tools listed: 30
- Resources listed: 3 static + 1 template
- Prompts listed: 3
- Total entries exercised: 37
- PASS: 37 / PARTIAL: 0 / FAIL: 0
- Run time: 152 ms
Listed tools
analyze_floorplan_image
analyze_room_photo
apply_patch
check_collisions
create_from_template
create_level
create_wall
cut_opening
delete_node
delete_scene
describe_node
duplicate_level
export_glb
export_json
find_nodes
generate_variants
get_node
get_scene
list_scenes
list_templates
load_scene
measure
photo_to_scene
place_item
redo
rename_scene
save_scene
set_zone
undo
validate_scene
Listed resources
(static)
pascal://catalog/items
pascal://scene/current
pascal://scene/current/summary
(templates)
pascal://constraints/{levelId}
Listed prompts
from_brief
iterate_on_feedback
renovation_from_photos
Pass matrix — tools
| # | Tool | Status | Note |
|---|---|---|---|
| 1 | list_templates |
PASS | 3 templates |
| 2 | create_from_template |
PASS | templateId=two-bedroom nodes=25 |
| 3 | get_scene |
PASS | 25 nodes / 1 roots |
| 4 | get_node |
PASS | type=site |
| 5 | describe_node |
PASS | type=site children=1 |
| 6 | find_nodes |
PASS | 1 level(s) |
| 7 | measure |
PASS | d=6.403m |
| 8 | apply_patch |
PASS | applied=1 |
| 9 | create_level |
PASS | levelId=level_7fjtu570j6yyxcm4 |
| 10 | create_wall |
PASS | wallId=wall_2b2og8j9nrr6jk6t |
| 11 | place_item |
PASS | itemId=item_efdmfnownqwa3yd4 |
| 12 | cut_opening |
PASS | openingId=door_c0cul6bctrwf76md |
| 13 | set_zone |
PASS | zoneId=zone_7ljy8xitu0km2q8d |
| 14 | duplicate_level |
PASS | newLevelId=level_i5yq9hk2kwd9unpw nodes=28 |
| 15 | delete_node |
PASS | deleted 28 nodes |
| 16 | undo |
PASS | undone=1 |
| 17 | redo |
PASS | redone=1 |
| 18 | export_json |
PASS | 21051 chars |
| 19 | export_glb |
PASS | bytes/b64=0 |
| 20 | validate_scene |
PASS | valid=true errors=0 |
| 21 | check_collisions |
PASS | 0 collision(s) |
| 22 | analyze_floorplan_image |
PASS | walls=4 rooms=1 conf=0.82 |
| 23 | analyze_room_photo |
PASS | w=4m fixtures=2 |
| 24 | save_scene |
PASS | id=7b7ca35340e3 v=1 nodes=31 |
| 25 | list_scenes |
PASS | 1 scene(s) |
| 26 | load_scene |
PASS | id=7b7ca35340e3 nodes=31 |
| 27 | generate_variants |
PASS | 2 variants, ids=2 |
| 28 | photo_to_scene |
PASS | sceneId=fbde55412851 walls=4 rooms=1 |
| 29 | rename_scene |
PASS | name=p10 base renamed |
| 30 | delete_scene |
PASS | deleted=86366d07b242 |
Pass matrix — resources
| # | Resource | Status | Note |
|---|---|---|---|
| 1 | pascal://scene/current |
PASS | 8 nodes / 1 roots |
| 2 | pascal://scene/current/summary |
PASS | 425 chars of markdown |
| 3 | pascal://catalog/items |
PASS | 0 items |
| 4 | pascal://constraints/{levelId} |
PASS | slabs=0 wallPolys=4 |
Pass matrix — prompts
| # | Prompt | Status | Note |
|---|---|---|---|
| 1 | from_brief |
PASS | 1 message(s) |
| 2 | iterate_on_feedback |
PASS | 1 message(s) |
| 3 | renovation_from_photos |
PASS | 6 message(s) |