mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-26 13:07:33 +09:00
Editor links with API access can edit text and editable props inside Content Blocks through raw CLI/MCP patches. Builder and Viewer permissions stay unchanged. UI and API requests use the same Content-mode policy. The API keeps its narrower text-and-props scope; page, asset, style, expression, and structure changes remain blocked for raw Editor patches. ### Implementation - Use the persistence patch engine for instance, prop, and style state changes. Remove the separate instance patch interpreter and record writes from permission validation. - Check prop changes against the resulting state. Image dimensions work even when their patches appear before the source patch. - Move Image source, dimensions, and alt updates into the shared runtime. Video source changes preserve its existing dimensions. The UI no longer sends separate follow-up writes. Explicit overrides and expression bindings are preserved. - Separate capability discovery from transaction validation and move the server permission adapter out of the router. - Reject missing replacement targets and protected or renamed prop-ID reuse. Keep template-copy protections. - This refactor removes 19 non-test lines. It removes eager whole-instance cloning, but the server still loads the project and validates its content tree. Empty transactions skip that validation. Closes #6573 ### Checklist - [x] Reproduce the Editor-token denial and add regression coverage - [x] Keep Editor raw patches limited to text and editable props - [x] Use the existing patch engine and test missing targets, immutable input, and permission boundaries - [x] Share media updates between UI and MCP; test atomic updates, patch ordering, overrides, and bindings - [x] Separate capability discovery, permission validation, and API orchestration - [x] Verify new regression tests fail with the relevant behavior disabled, then pass after restoration - [x] Run runtime tests, API/sync tests, typechecks, lint, formatter, package-boundary, and generated-API checks - [x] Run Content-mode E2E checks for templates, assets, page creation, collaboration, and save/reload - [x] Add a CLI MCP Editor-token save/reload E2E, including protected template rejection - [x] Return a structured forbidden error for rejected Editor MCP content changes - [x] Run the complete Luna/low evaluation suite; record failures below - [x] Review documentation and fixture impact: no new API inputs, schema changes, migrations, or fixture generation changes; existing guidance remains applicable - [x] Correct the project-package permission fixture to include its owning instance and run all package tests - [x] Cover repeated media source updates and bound namespace batches in Editor requests - [x] Preserve stale retry handling before Editor content validation, with API regression coverage - [x] Preserve expression-bound media metadata during source replacement and test saved Content-mode output - [x] Reuse batch merging so split prop payloads validate against their final state; test split and combined forms - [x] Push the refactor and verify required CI completes - [ ] Deferred: reproduce the separate Builder UI report on a session-free Editor link - [ ] Deferred: confirm image-description editing expectations separately ### Verification - Full project-build suite: 2,597 tests passed. Focused permission/prop run: 121 passed. - All package tests (`pnpm test:packages`) passed after correcting the project-package fixture. All 1,297 CLI tests passed. - Builder API and sync authorization: 54 passed; settings-panel tests: 11 passed. - Content-mode E2E: 9 passed, including setup, plus the earlier focused Image replacement run. - Builder/project-build typechecks, lint, formatting, package boundaries, and generated-API checks passed. The CLI content-runtime bundle test passed; no generated fixture output changed. - Earlier full Luna/low evaluations: **7/9 passed**. `managed-form-runtime-v1` failed `runtimeFormAction`; `markdown-blog-v1` failed `blogRouteEvidence`. These checks also failed before this refactor. MDX editing and Markdown-reference discovery passed. Baselines for this model/effort were incompatible or missing and were not updated. This suite is not fully green. - Latest full Luna/low rerun after review fixes: **6/9 passed**. Form runtime, blog authoring, and MDX editing failed; the MDX fixture passed in the earlier run. Baselines were not updated. The evaluation suite remains incomplete. - Retry follow-up: all 59 API/router tests and 28 persistence patch tests passed, along with Builder typecheck and lint. Older retries use existing persistence handling; current and future versions remain permission-checked. - Bound-media follow-up: Image and Video source replacement preserves expression-bound width/alt and still passes shared Content-mode validation. Full runtime tests, typecheck, and lint passed after this fix. The agent evaluation results above precede the final retry and binding guards; those guards have dedicated regression coverage. - Split-payload follow-up: reuse the existing batch merge helper before permission validation. Combined and split media patches pass the same final-state checks. Full runtime tests, 59 API tests, typechecks, and lint passed. Agent evaluations were not repeated after this follow-up. - CI passed on `1242ba8344`: all package, Builder, and CLI test jobs; all six Builder E2E shards; visual regression; typecheck/lint; generated-file checks; packaged SSG; size; migrations; and submodule checks. Development deployment checks passed. All review threads are resolved. ### Final review follow-up (2fbb30ffe6; CI passed) - [x] Reject future base versions before saving; test an otherwise allowed Editor patch - [x] Preserve sequential patch behavior and test authorization/persistence parity - [x] Run focused tests, typechecks, lint, and review documentation/fixture impact - [x] Commit and push these follow-up fixes, then verify CI Local follow-up verification: 2,600 project-build tests, 96 project tests, and 60 API tests passed. Builder/project-build typechecks, changed-file lint and formatting, package boundaries, and diff checks passed. Regression tests failed without the fixes and passed with them. The shared patch engine uses one Immer draft and supports namespace replacement without mutating input. Net production change: 6 lines. No documentation, fixture inputs/outputs, schemas, or generated contracts changed. Browser E2E and agent evaluations were not rerun for this follow-up; the race and patch-order cases are covered by deterministic tests. CI passed on `2fbb30ffe6`: all six Builder E2E shards, package/CLI/Builder tests, typecheck/lint/generated checks, visual regression, migrations, SSG build, size, and submodule checks. No new blocking findings in the follow-up review. Agent evaluations were not rerun; earlier limitations remain recorded above. ### Asset review follow-up - [x] Reproduce and block asset-valued design-only props, retaining matching Image metadata - [x] Stop generating image metadata for Video source updates - [x] Verify regression tests, documentation/fixture impact, and CI; reply to both review threads Asset follow-up `2ea28028c0`: shared metadata eligibility now checks asset values and their matching Image source, including MDX materialization. Video source edits do not generate Image-only metadata. New and updated regression tests failed without the fixes and pass with them. All 2,604 runtime tests, 60 API tests, 30 persistence tests, both typechecks, lint, formatting, and package boundaries passed. No documentation, schemas, or fixture data changes are needed. Agent evaluations were not rerun. Both review threads have replies and are resolved. CI passed on `2ea28028c0`, including all six E2E shards, visual regression, package/CLI/Builder tests, typecheck/lint/generated checks, migrations, SSG build, size, and submodules. - [x] Reproduce repeated source updates without a saved source, coalesce before ID assignment, and verify regression tests/CI Final source follow-up `16c1c5f68e`: repeated instance/property updates are coalesced before assigning IDs, replacing the later source-only check. Both new Image/Video regression cases failed before the fix and pass now. All 2,606 runtime tests, typecheck, and lint passed. CI passed on this commit, including all six E2E shards and visual regression. No documentation or fixture-data changes were needed; evaluations were not rerun. ### Coverage audit - [x] Audit changed behavior against assertions, including UI/API and persistence boundaries - [x] Add test assertions for identified coverage gaps; automatic CI passed - [x] Review documentation and fixture impact: no product documentation change; one new SVG used only by browser E2E Per request, no local tests or agent evaluations were run for this audit. Automatic CI passed on `4591940169`, including package/API tests, all six browser-test shards, visual regression, typechecks, lint, and generated-file checks. Coverage additions are pushed in `2371d9796a` and `4591940169`: check actual text sent to persistence; duplicate ordinary props across instances; rejected edits leaving capabilities reusable; merged patch ordering and input immutability; accepted/rejected linked Image dimensions in MDX templates; returned style/source/selection state; and UI Image dimension changes followed by reload. The previous image E2E used equal-sized files and could miss broken dimension synchronization. Its new SVG has different dimensions. Production code is unchanged. No tests or evaluations were run for this audit, as requested; no new pass claim is made. - [x] Inventory every changed/moved/removed production function and map it to assertions - [x] Add missing mixed-child and dependent-transaction coverage - [x] Push inventory follow-up and inspect CI results ### Function-by-function coverage inventory This inventories production functions changed, moved, or removed by this PR. “Covered” means the named test asserts the behavior; it does not claim every possible input or branch is covered. No local tests or evaluations were run for this inventory. | Function / handler | Assertions traced | | --- | --- | | `renderProperty` and its `onChange` callback | Content-mode props E2E checks text, href, Image/Video source, alt, protected controls, and reload. Asset E2E now changes dimensions from 120×80 to 240×160, then replaces/reloads to 120×80. | | `createContentModeCapabilitiesFromBuild` (moved unchanged) | API “commits content-mode mutations with editor api tokens” and “limits editor api patches to content instances and props” exercise conversion from saved build data. | | `assertContentOrBuildTransactions` | Editor vs Builder, design denial, transaction/payload limits, all-or-nothing rejection; new exact 10-transaction boundary and dependent append-then-edit transactions. | | `contentOrBuildMutation` | API semantic text edit asserts version, authorization, actual saved text, unchanged outside content, and unchanged input. | | `build.patch` handler | API tests assert allowed text forms, denied structure/styles/assets/expressions/outside roots, Viewer/no-API denial, stale retries, current-version validation, and future-version CONFLICT before persistence. | | `getContentModePropNamesByTag` (moved unchanged) | Derives names by tag and merges names for matching tags. | | `getDefaultContentRootIds` (moved unchanged) | Default vs explicit roots, Body selection, template exclusion. | | `getContentModeFrontmatterBoundTargets` (moved unchanged) | Explicit Body/frontmatter scope, read vs readwrite expressions, recursive instances. | | `getContentModeEditableInstanceIds` (moved unchanged) | Outside roots, nested templates, missing references/roots, non-id children, cycles. | | `getContentModeEditableInstanceRootIds` and its recursive `visit` (moved unchanged) | Same traversal cases; transaction tests reject moving content between roots and duplicate references. | | `getInstancePropMetas` (moved unchanged) | Tag-derived props, unknown/design-only/incompatible props, no generic HTML-attribute permission bypass. | | `getContentModeEditableStaticPropNames` (moved unchanged) | MDX template tests assert exact editable names, primitive conversions, and retained/ignored design-only props. | | `getContentModePropEligibility` | Matching/missing/different asset sources, protected asset props, exact primitive types, UI/transaction parity, Image vs Video. | | `isContentModeCopyableProp` (moved unchanged) | Editable/copyable classification and content-mode copy-policy test. | | `getContentModeCapabilities` | Editable IDs, roots, frontmatter bindings, asset metadata; shared UI and transaction eligibility test. | | `isContentModePropTransition` | Schema/id/owner/name checks, unknown instance, protected existing props, permitted source type changes, linked dimensions. | | `validatePropPatches` | Add/replace/remove, new-then-update, missing replacement target, protected ID reuse, copied template defaults, final-state media patches in split/reversed order, failed-edit immutability and retry. | | `hasSameInstanceMetadata` | Whole-instance text replacement allowed; tag/name changes and outside references rejected. | | `validateStyleSourceSelectionPatch` | New content selection allowed; existing/token misuse and dangling cleanup rejected; now asserts resulting selection map. | | `validateStyleSourcePatch` | New local source allowed; token, duplicate/unused source and incomplete cleanup rejected; now asserts resulting source map. | | `validateStylePatch` | New local declarations allowed; existing design styles, missing breakpoint and incomplete cleanup rejected; now asserts resulting declaration map. | | `validateMapChange` | Above three map paths, invalid patch targets, resulting state and input immutability. | | `validatePatchChange` | Instance/prop/style/asset/page paths and rejected design namespaces through transaction tests. | | `preservesNonTextChildren` and local `keepNonText` | Text-only changes allowed; insertion/deletion/reordering of instance references and expression changes denied; new mixed text/reference positional cases. | | `applyContentModeTransaction` | Authorization/persistence parity, split batches, empty transactions, scoped permissions, copy protections, roots, cleanup, and immutable input on success/failure. | | `materializeMdxAuthoredContent`, its `visit`, and `overlayChildren` | MDX-source “round-trips an authored Asset Image through a styled composite descendant” asserts src/width/height and styles after reload; “serializes an Asset selected on an inserted Image template” asserts all four linked props; nested and standalone component-permission tests reject protected props. | | `getMdxPropEligibility` | Template tests check allowed/ignored props, aliases, incompatible types; new matching vs mismatched Image dimension asset test exercises passed asset values. | | `materializeMdxTemplates` | New linked-dimension test plus existing copied-fragment, unchanged-input, authored-prop, diagnostic, and alias tests. | | `appendBuilderPatchChanges` | Mutation accumulator test asserts sequential state and combined payload; merge test asserts ordering and input preservation. | | `mergeBuilderPatchChanges` | New repeated/interleaved namespace, empty input, repeat-call and patch-array immutability assertions; permission tests exercise split payload integration. | | `getMediaAssetPropNames` | Image generates width/height/alt; Video and unrelated components do not; explicit overrides and expression-bound metadata retained. | | `getPropIdsToDelete` | Image src removes dimensions; non-Image removes only requested prop; resource cleanup and missing-instance cases. | | `updateProps` | Atomic Image/Video updates, repeated sources with/without saved props, overrides, bindings, original-state preservation; new ordinary prop deduplication across names and instances. | | `getPatchParent` (type-only change) | Mutable nested patch and missing-path tests. | | `assertReplacePatchTargetsExistingPath` (type-only change) | Missing target rejected; preceding add/root replacement makes later replacement valid; preceding remove makes it invalid. | | `applyBuilderNamespacePatches` | Sequential/root replacements, append then edit, remove then replace, immutable state, permission/persistence parity. | Removed functions are covered through their replacements: `shouldSyncMediaAssetProps` → shared `updateProps` plus UI E2E; `assertContentOrBuildPayload` → transaction adapter; `isContentModePropForInstance` → eligibility; `isContentModePropPatchValue`/`validatePropPatch` → transition/batch validation; `applyValidatedInstancePatch` → shared patch engine. No behavior test is needed for import-only changes, type declarations, public-entry exports, or regenerated metadata/docs; their callers and contracts retain tests. The original reporter-specific UI reproduction and image-description product question remain separate limitations. This inventory does not claim either is resolved. ### Latest review follow-up - [x] Reject fresh prop IDs that shadow existing instance/property records; cover protected bindings and valid edits - [x] Coalesce updates in linear time while preserving last-update order and input immutability - [x] Check documentation/fixture impact, push, inspect CI, and reply to both review threads Review fixes pushed in `5dd03135e7`: 10 net production lines. Added binding-shadow and batch-work regression assertions; no local tests or evaluations were run, per request. Formatting and diff checks passed. Documentation and generated fixture inputs/outputs are unchanged. Automatic CI is pending. - [x] Reject duplicate fresh props within the same transaction and test split/combined batches - [x] Preserve resource and parameter media bindings using the existing dynamic-type check - [x] Bound total Editor patches before validation; test the boundary and unchanged Builder access Review follow-up `d478446eb1`: all package, Builder, and CLI CI test jobs and typecheck/lint/generated checks passed. Five addressed review threads are resolved. Browser CI checks are still running. One newly posted review about non-image assets assigned to Image sources remains open pending the user's behavior choice. No local tests or agent evaluations were run; local typechecks passed. ### Shared asset constraints - [x] Reuse component file-control metadata and the UI MIME matcher in shared property writes; preserve manually entered URLs and pending uploads - [x] Generate linked Image metadata only when the selected asset supports it; cover legacy file-backed images - [x] Add shared-runtime coverage for accepted/rejected Image and Video assets, custom props, URLs, and atomic rejection - [x] Review documentation, generated contracts, fixture impact, and diff size; verify typechecks and CI (no local tests or evaluations, as requested) Shared asset follow-up `5f0054edd7`: property writes use the UI's existing file-control `accept` metadata and MIME matcher. Known incompatible assets are rejected with the approved message. Legacy image files remain selectable without generating unsupported metadata. URLs, custom asset props, and unresolved uploads keep their existing behavior. Asset records are now loaded before CLI/MCP property writes; this may require an asset-list request when not already loaded. This change does not add asset-file downloads. Net handwritten production change: 24 lines. Builder/project-build typechecks, changed-file lint, formatting, and diff checks passed. No local tests or evaluations were run. Regression and runtime-entry tests were added; CI is pending. Reviewed Image/Video docs: no guidance change needed. No fixture inputs or generated fixture bundles changed; regenerated runtime contract metadata only. - [x] Keep Editor permission checks when a shared edit operation loads assets; add an API regression test before final CI verification Follow-up `d33b70e1c7` preserves Editor authorization when property writes load assets. The API regression test asserts a valid Image source reaches persistence, incompatible files are rejected, and outside-content edits remain forbidden. This reuses the existing asset loader and permission adapters (3 net production lines). CI found and corrected an old session namespace expectation and a mistaken test assumption: Video's existing unsupported `.mpg`/`.mpeg` accept entries cause the shared matcher to allow all assets. This PR preserves that UI behavior; it does not tighten Video formats. Total handwritten production increase for this asset follow-up: 27 lines. Builder typecheck/lint and server metadata generation passed; metadata output was unchanged. CI is rerunning. Final asset follow-up verification (`8b2efd8508`): all package, CLI, Builder API/unit, and six browser E2E jobs passed, together with lint/typechecks, generated checks, visual regression, migrations, size, packaged SSG, and branch deployment. The direct API test now checks the typed authorization cause, not an HTTP status that this test caller does not map. All review threads are resolved. No local tests or agent evaluations were run for this follow-up; CI provided execution coverage. The separate Video filter limitation noted above remains unchanged. ### Shared write-boundary validation - [x] Reuse one asset-property validator in UI/MCP property updates and raw patch persistence, independent of permission level - [x] Cover whole-property and field patches, component changes, invalid metadata, final-state validation, unchanged legacy data, and stale retries - [x] Review documentation/fixture impact, typecheck and lint, then verify CI; no local tests or evaluations per the earlier request Expected change: about 70–90 net production lines. Patch saves load only referenced asset metadata; no asset-file downloads. Shared-save follow-up: 133 net production lines, above the initial estimate because one save can replace an asset file and its references together. The save validator reuses the asset writer’s canonical file preparation rather than trusting client-declared asset types. It also checks asset-only replacements, before folder or Build writes, while leaving unchanged legacy references alone. Ordinary property writes load only referenced asset metadata; asset-changing saves with references additionally prepare the full asset map, then the writer repeats that metadata preparation. No file contents are downloaded by the new check. This does not change schemas, migrations, generated contracts, documentation, or fixture inputs/outputs. Added regression coverage; no local tests/evaluations were run, per the earlier request. Independent Astra review found no remaining defect after the projection fix. CI pending. - [x] Fix selective UI sync loading to include asset-validation dependencies; add selection regression coverage and rerun CI The first browser run caught missing props/instances in selectively loaded UI saves. Package/API tests passed but their fixtures provided complete build rows. The development deployment succeeded; its status-reporting step failed with GitHub HTTP 500. Final verification on `a22d059717`: all package, CLI, Builder unit/API, six browser shards, visual regression, typecheck/lint/generated checks, migrations, size, packaged SSG, development deployment, and Ubuntu/Windows deployment fixtures passed. Browser shard 3 passed on one retry after a page-drag ordering assertion; the save/parsing errors were fixed by loading the required fields. No local tests or evaluations were run. The added persistence suite has 46 passing tests and runtime props has 74; selective-sync tests now omit unselected fields. No unresolved review threads. Total net production increase across this follow-up: 134 lines. ### Deep review follow-up - [x] Make MDX linked-image validation independent of authored attribute order; cover changed sources, mismatches, and the affected materialization paths - [x] Review DRY and diff size, documentation/fixture impact, and independent review findings - [x] Verify typechecks/lint and CI; no local tests or evaluations per the earlier request Review found that linked dimensions can be checked against the old template source before the new source is applied. Fix should reuse the shared eligibility rule with the final source, without another property parser. Initial estimate: under 60 added non-test lines; revisit the approach if it needs more. Deep-review fix `f7c3d805a8`: apply Image sources before linked metadata in all three MDX materialization paths, keeping original attribute indexes for asset references. Reuses the existing eligibility checks and parser; 23 net production lines. Regression cases cover source order, changed sources, mismatched dimensions, and nested overlays. Independent Astra review, project-build typecheck, focused lint, formatting, and diff checks passed. No schema, documentation, generated contract, or fixture-data changes. Local tests/evaluations have not run; focused-test permission was requested. CI pending. Final deep-review verification on `f7c3d805a8`: all package/CLI/Builder test jobs, all six browser shards, visual regression, typechecks/lint/generated checks, migrations, packaged SSG, size, submodules, and development deployment/fixtures passed. CI ran 60 authored-content and 24 template-materialization tests successfully. No local tests or evaluations ran; fail-before/pass-after was not executed locally. All review threads are resolved and the worktree is clean. No further actionable defect was found in the independent reviews. Known remaining cost: asset-changing saves can prepare metadata twice; removing that safely would expand this fix into persistence changes. ### Editor MCP browser E2E follow-up `0ee978e465` adds a real CLI/MCP Editor-token round trip: insert content into a Content Block, reload through a fresh CLI process, verify it in the Builder canvas, and reject a protected template insertion without changing the saved Build. A direct API denial now returns `FORBIDDEN` instead of a generic internal error. The focused E2E and API tests, typecheck, lint, formatting, and diff checks passed locally. Fail-before/pass-after was verified by temporarily disabling Editor fragment access, then restoring it. The full Luna/low evaluation run was attempted but did not pass: the form-runtime fixture received 503 responses. The suite is not claimed green. CI for this push passed on `0ee978e465`, including all six Builder E2E shards, package/CLI/Builder tests, visual regression, typecheck/lint/generated checks, migrations, size, SSG build, and submodules. Live MCP permission probe against the branch deployment: Builder, Editor, and Viewer role metadata matched their tokens. Editor content text, rich text, fragment, link href, and compatible Image source edits committed and survived a fresh MCP session. Outside-root, protected-template, design-only prop, mismatched Image width, and incompatible asset writes were denied without persistence. The updated deployment returned structured `FORBIDDEN` for protected content writes even when `contentMode` was false or omitted. Mixed valid/invalid writes were atomic; stale Editor writes refreshed and retried. Viewer MCP could not be exercised beyond the API gate because this Viewer token has API access disabled. MDX frontmatter, token revocation, and UI-only scenarios were not tested. A wrong-project token was rejected but surfaced `INTERNAL_SERVER_ERROR`, which is a diagnostic-quality gap, not an authorization bypass. The isolated probe page `/codex-permission-probe-6575` remains: cleanup was stopped after a concurrent-edit `CONFLICT`, to avoid deleting another session's work. ### Fixture-based parity follow-up - [ ] Verify the full Content-mode UI/MCP parity matrix with an isolated MDX/template/media/page fixture, excluding publishing and token/plan changes - [x] Fix the reproduced Editor MCP template/page-settings gap and cover fail-before/pass-after routing - [x] Verify a real CLI Editor-token create-from-template call, fresh readback, Builder reload, allowed settings, and protected-setting rejection - [x] Update MCP guidance, review fixture impact, and verify required CI The UI already allows an Editor to create a page from a template and edit allowed page settings. MCP incorrectly required Builder permission, and a separate MCP-only legacy Webhook Form copy guard blocked the seeded template. Those restrictions are corrected. The server now derives Content-mode page copying from Editor permission even when `contentMode` is omitted or false; Builder copies retain their existing behavior. Editor semantic commands use the server's shared Content-mode validator, while raw Editor patches stay limited to text and editable props. This adds a named server call and refetch for Editor semantic edits; the existing initial project sync still occurs. The new isolated E2E passed on `03e7ca6953`, including flag-free template creation, settings edit, fresh CLI readback, Builder reload, and a denied protected setting without a saved change. All required CI passed on that commit: all six Builder E2E shards, package/CLI/Builder tests, generated checks, typecheck/lint, visual regression, migrations, packaged SSG, size, and submodules. Focused local runtime/API tests, typechecks, lint, formatting, and generated-doc checks passed. Browser E2E and full agent evaluations were not run locally; the requested browser approval and a fresh evaluation approval were not received. The full UI/MCP parity matrix remains open; Editor asset operations are a separately identified gap, not claimed fixed here. ### Exploratory permission coverage follow-up - [x] Inventory observed Builder/Editor/Viewer UI and MCP permission scenarios against existing tests - [x] Add fail-before/pass-after regression tests for confirmed uncovered failures - [x] Run focused tests, typechecks, lint, and fixture checks; verify required CI - [x] Record unsupported UI/MCP parity as open product work, without calling the matrix complete The UI share-link E2E already checks Viewer read-only controls, Editor text editing, and Builder design controls. Existing MCP E2E checks Editor content/template writes, page creation, asset upload, and protected edits. This follow-up adds one real CLI/MCP flow with API-enabled Viewer and Builder tokens: Viewer can list pages but cannot create a page or asset folder or upload a file; Builder can change an HTTP status setting that Editor cannot. The test checks the denied page writes leave the build unchanged and the Builder setting survives readback. The run exposed two diagnostic bugs: local permission denial and REST asset-upload denial reached MCP as generic failures. Both now retain `FORBIDDEN`. Focused unit tests failed before these changes and pass after; the full local browser/MCP test passes. The only non-test change is three net lines. No fixture data, schemas, generated contracts, or product docs changed. Local checks passed: 2,654 project-build tests, 48 HTTP-client tests, the focused Builder E2E (2 tests including setup), affected typechecks, lint, formatting, and package boundaries. `check:fixture-sizes` could not run locally because the SSG fixture's built `dist/client` is absent; CI builds that fixture before checking its size. The full UI/MCP parity matrix remains open. In particular, Editor UI asset update/replacement/duplication/deletion and collection-entry creation still lack equivalent MCP support until the UI's collection protections are enforced at the shared server boundary. Publishing and token/plan changes were outside this test scope. CI passed on `6490efc838`: all six Builder E2E shards, package/CLI/Builder tests, visual regression, typecheck/lint/generated checks, migrations, packaged SSG, size, and submodules. Shard 3 first hit Playwright's 120-second limit under four-worker load; its logic passed locally. The longer role flow now uses the same 240-second per-test limit as other MCP E2E tests, and shard 3 passed in the final run. ### Final UI/MCP parity audit - [x] Trace Editor UI asset and folder actions through REST, runtime, and persistence - [x] Reproduce Editor MCP upload and folder-create denials; add fail-before/pass-after permission tests - [x] Add isolated Editor MCP upload/folder and UI folder-reload browser regressions - [x] Keep collection-sensitive asset/folder mutations Builder-only until UI collection protections are shared at the server boundary - [x] Review docs and fixture impact: MCP guidance is updated; no fixture input or bundle changes - [x] Restrict Editor folder saves to new IDs; test overwrite and delete denials at both UI and API boundaries - [x] Reject folder-add collisions at persistence and test the mixed-writer/stale-snapshot case - [x] Keep exact Editor folder retries authorized after a partial save; add fail-before/pass-after coverage - [x] Verify the new browser E2E in required CI - [ ] Run the full Luna/low agent evaluations after fresh approval (not granted for this follow-up) - [x] Verify required CI for `6fd3beefc8` The safe scope is small: the Assets REST upload path already authorizes Editor tokens and enforces collection rules, but the MCP catalog incorrectly required Builder access. Creating an ordinary asset folder is available in the Editor UI, but the shared Content-mode save check rejected it. Both now use Editor permission. The isolated browser tests check fresh MCP readback and Builder reload; they have not run yet. Further parity work remains open: Editor UI allows updates, replacement, duplication, deletion, and collection-entry creation in cases MCP cannot currently perform. These need the UI's collection protections enforced at the shared server boundary before their MCP permits can be widened. Raw Editor `apply-patch` stays text-and-props-only. This PR does not claim the full matrix is complete. Asset/folder follow-up `6fd3beefc8`: Editor semantic tools now share the UI's edit permission for uploads and ordinary folder creation. Folder saves reject same-ID changes, allow identical retries after a lost response, and use insert for add-only persistence. The retry regression failed before the fix and passes with JSON-normalized parent IDs/timestamps. Independent Astra review found no remaining concrete defect. Focused runtime (86), asset persistence (52), protocol (14), and API/sync (71) tests passed; Builder/project-build/asset-uploader typechecks, lint, package boundaries, formatting, generated API, and MCP docs checks passed. Full project-build, protocol, and CLI suites passed earlier in this follow-up; Builder core had resource-contention failures but its affected tests passed in isolation. The new browser E2E and agent evaluations were not run locally pending fresh approval; CI is running them automatically. CI passed on `6fd3beefc8`: all six Builder browser shards, package/CLI/Builder tests, generated checks, typechecks, lint, visual regression, migrations, packaged SSG, size, and submodules. The new Editor MCP asset round trip and UI folder-reload tests ran in the passing browser shards.