Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.
Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
@types/react, @types/react-dom, @types/three to prevent JSX namespace
fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
match the new versions on every bump (so viewer/editor/mcp tarballs always
reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts
Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
what private-editor currently reports
Open PRs against editor-v2 will need rebasing/conflict resolution.
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>
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>
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>
- 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>