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>
2.4 KiB
2.4 KiB
Phase 8 P1 — templates lifecycle (stdio MCP)
Generated: 2026-04-19T18:18:14.719Z
Transport: stdio (bun packages/mcp/dist/bin/pascal-mcp.js --stdio), data dir /tmp/pascal-phase8-p1.
Summary: 18/18 PASS, 0 FAIL, 100 ms.
Steps
| # | Step | Status | Detail |
|---|---|---|---|
| 1 | list_templates | PASS | ids=[empty-studio,garden-house,two-bedroom], empty-studio(10), two-bedroom(25), garden-house(18) |
| 2a/empty-studio | create_from_template + save_scene | PASS | templateId=empty-studio, createdNodes=10, sceneId=58d29341f2ac, version=1 |
| 2b/empty-studio | validate_scene | PASS | valid=true, errors=0 |
| 2c/empty-studio | get_scene counts | PASS | nodes=10, zones=1, walls=4, doors=1, windows=1 |
| 2a/two-bedroom | create_from_template + save_scene | PASS | templateId=two-bedroom, createdNodes=25, sceneId=5e1fbd0fd735, version=1 |
| 2b/two-bedroom | validate_scene | PASS | valid=true, errors=0 |
| 2c/two-bedroom | get_scene counts | PASS | nodes=25, zones=4, walls=9, doors=4, windows=5 |
| 2a/garden-house | create_from_template + save_scene | PASS | templateId=garden-house, createdNodes=18, sceneId=b11de24c35c5, version=1 |
| 2b/garden-house | validate_scene | PASS | valid=true, errors=0 |
| 2c/garden-house | get_scene counts | PASS | nodes=18, zones=2, walls=4, doors=2, windows=4 |
| 3 | list_scenes | PASS | scenes=3, names=[p1-empty-studio,p1-garden-house,p1-two-bedroom] |
| 4 | measure between zones | PASS | from=zone_q04o6614gj1k6025, to=zone_zyts5lliy88xf7tj, distance=5.000m |
| 5/empty-studio | delete_scene | PASS | id=58d29341f2ac, deleted=true |
| 5/two-bedroom | delete_scene | PASS | id=5e1fbd0fd735, deleted=true |
| 5/garden-house | delete_scene | PASS | id=b11de24c35c5, deleted=true |
| 5/final | list_scenes empty | PASS | remaining scenes=0 |
| 6a | create_from_template unknown id | PASS | tool_error text="MCP error -32602: unknown_template: nonexistent. Call list_templates for the set of valid ids." |
| 6b | load_scene missing id | PASS | tool_error text="MCP error -32602: scene_not_found" |
Per-template snapshot (step 2c)
| Template | Scene name | nodes | zones | walls | doors | windows |
|---|---|---|---|---|---|---|
| empty-studio | p1-empty-studio | 10 | 1 | 4 | 1 | 1 |
| two-bedroom | p1-two-bedroom | 25 | 4 | 9 | 4 | 5 |
| garden-house | p1-garden-house | 18 | 2 | 4 | 2 | 4 |