|
| 1 | +--- |
| 2 | +title: Source-safe, proof-carrying WebMCP edits in Studio |
| 3 | +date: 2026-09-01 |
| 4 | +category: integration-issues |
| 5 | +module: studio-webmcp |
| 6 | +problem_type: integration_issue |
| 7 | +component: assistant |
| 8 | +symptoms: |
| 9 | + - Agent writes could follow the human selection instead of the supplied handle |
| 10 | + - Duplicate nested element ids could resolve to the wrong source file |
| 11 | + - A completed tool call could prove dispatch without proving persistence |
| 12 | + - Saved edits could leave mounted thumbnails stale |
| 13 | + - Studio did not distinguish inspection, writing, saving, and verification visually |
| 14 | +root_cause: scope_issue |
| 15 | +resolution_type: code_fix |
| 16 | +severity: high |
| 17 | +related_components: |
| 18 | + - tooling |
| 19 | + - studio-server |
| 20 | + - thumbnail-cache |
| 21 | + - dom-editing |
| 22 | +tags: |
| 23 | + - webmcp |
| 24 | + - agent-editing |
| 25 | + - target-handles |
| 26 | + - edit-receipts |
| 27 | + - source-ownership |
| 28 | + - topology-lens |
| 29 | + - cache-invalidation |
| 30 | + - undo |
| 31 | +--- |
| 32 | + |
| 33 | +# Source-safe, proof-carrying WebMCP edits in Studio |
| 34 | + |
| 35 | +## Problem |
| 36 | + |
| 37 | +Studio's WebMCP edit path looked explicitly targeted, but identity, ownership, ordering, and proof were implicit. A handle could address one element while a later layer consulted mutable human selection or project state, and a resolved promise could be mistaken for durable persistence. |
| 38 | + |
| 39 | +## Symptoms |
| 40 | + |
| 41 | +- Repeated authored ids in nested compositions could resolve to the wrong source without source ownership in the handle. |
| 42 | +- A source-scoped handle could be replayed after switching projects if project ownership was not encoded and rechecked. |
| 43 | +- Overlapping edits could read the same original bytes and record incorrect Undo boundaries. |
| 44 | +- Animation handlers that discard persistence outcomes could look saved merely because dispatch completed. |
| 45 | +- Thumbnail generation could observe newer content and still cache those pixels under an older signature. |
| 46 | + |
| 47 | +## What Didn't Work |
| 48 | + |
| 49 | +- Treating a DOM id or selector as identity. A locator says what matches now, not which authored element the caller meant. |
| 50 | +- Adding source scope without project scope. The handle remained replayable in another project with matching paths and ids. |
| 51 | +- Queueing only the HTTP write. The consistency boundary also includes the read, patch decision, history record, and refresh. |
| 52 | +- Using `await` as durability evidence. An awaited handler can still prove only that work was handed off. |
| 53 | +- Resolving explicit agent targets through human selection caches. UI state is not ownership evidence. |
| 54 | +- Checking a cache signature only before asynchronous generation. The content can change while rendering is in flight. |
| 55 | + |
| 56 | +## Solution |
| 57 | + |
| 58 | +Treat every agent edit as a proof-carrying transaction with five parts: |
| 59 | + |
| 60 | +```text |
| 61 | +identity -> ownership -> serialization -> evidence -> cache invalidation |
| 62 | +``` |
| 63 | + |
| 64 | +### Bind the handle to every ownership boundary |
| 65 | + |
| 66 | +Writable handles encode the project, active composition, source file, and element address. Writes reject legacy or foreign-project handles before resolution. Reads keep legacy compatibility, but project-scoped read handles are refused when their project no longer matches. |
| 67 | + |
| 68 | +Re-resolve the handle against the live preview for every call. If the preview reloads during an awaited source probe, retry once and report a transient change rather than misclassifying the target as permanently stale. |
| 69 | + |
| 70 | +### Make one queue own the whole persistence transaction |
| 71 | + |
| 72 | +The queued actor owns this complete sequence: |
| 73 | + |
| 74 | +```text |
| 75 | +read current bytes -> validate -> patch -> record history -> refresh |
| 76 | +``` |
| 77 | + |
| 78 | +The project id is captured when work enters the queue. The worker refuses to start if the active project changed, and it never chooses a write destination from a mutable project reference. A conflict breaker is rechecked when each queued item starts, so work already waiting cannot slip past a newly opened pause. |
| 79 | + |
| 80 | +### Report the strongest proof actually available |
| 81 | + |
| 82 | +Receipts separate lifecycle from whether bytes changed: |
| 83 | + |
| 84 | +| Stage | Proven fact | |
| 85 | +| --- | --- | |
| 86 | +| `refused` | No write actor ran | |
| 87 | +| `dispatched` | The actor accepted the request | |
| 88 | +| `saved` | Versioned persistence evidence exists | |
| 89 | +| `verified` | Persistence plus independent readback matched | |
| 90 | +| `failed` | An actor ran and failed | |
| 91 | + |
| 92 | +Fire-and-forget animation handlers remain `dispatched`. No-op saves preserve `changed: false`, and the Topology Lens does not show a success seal for them. |
| 93 | + |
| 94 | +### Resolve animation state from the target |
| 95 | + |
| 96 | +Animation ownership and geometry routing use the explicit target's source file and element identity. The human selection cache is reusable only when it belongs to that exact live element. Raw JavaScript expressions are rejected at both the WebMCP input boundary and the server mutation boundary. |
| 97 | + |
| 98 | +### Revalidate cache identity after generation |
| 99 | + |
| 100 | +Thumbnail generation captures a project signature before rendering and computes a fresh signature afterward. Changed identity returns pixels to the current caller but skips the cache write. The metadata fingerprint includes change time, closing the same-size edit case where modification time is restored. |
| 101 | + |
| 102 | +## Why This Works |
| 103 | + |
| 104 | +- The handle is a capability, not merely a locator. |
| 105 | +- The persistence queue is the linearization point, so each edit observes the bytes produced by the prior edit. |
| 106 | +- Receipts carry epistemic state. Requested, saved, and independently verified are different claims. |
| 107 | +- Ownership follows the command target, not ambient UI state. |
| 108 | +- Cache keys prove the pixels filed under them. |
| 109 | + |
| 110 | +The Topology Lens is derived from the same receipt lifecycle, so its visual language cannot outrun the evidence. It lives in Studio chrome and never enters the composition, frame endpoint, thumbnail, or render output. |
| 111 | + |
| 112 | +## Prevention |
| 113 | + |
| 114 | +- Test duplicate ids in different nested source files and assert distinct handles and writes. |
| 115 | +- Switch projects during resolution and while a save is queued. Assert zero writes to the new project. |
| 116 | +- Break an animation ownership fetch and assert a structured pre-dispatch receipt, not a rejected tool promise. |
| 117 | +- Overlap two writes and assert the second Undo snapshot begins at the first write's result. |
| 118 | +- Open a conflict breaker with work already queued and assert that queued work never starts. |
| 119 | +- Change project content during thumbnail generation and assert the generated pixels are not cached under the prior signature. |
| 120 | +- Capture Studio, the composition frame, and a thumbnail while the Topology Lens is active. Only Studio should contain it. |
| 121 | + |
| 122 | +## Related Issues |
| 123 | + |
| 124 | +- [WebMCP edit transaction contract](../../contracts/2026-08-31-studio-webmcp-edit-transaction.html) |
| 125 | +- [Let an agent drive Studio](../../guides/webmcp.mdx) |
0 commit comments