From b845a285e774cb3208cf6bbef03df2c57357709c Mon Sep 17 00:00:00 2001 From: Ashish Huddar Date: Sat, 9 May 2026 23:41:47 +0530 Subject: [PATCH] fix(canvas): duplicate canvas ids must keep newest payload (greptile P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readCanvases sorts file canvases newest-first by updatedAt. The canvases route then merged them via Map.set in iteration order, which overwrites earlier (newer) entries with later (older) ones. If an agent wrote `result-v1.json` and `result-v2.json` with the same canvas id, the user would see the OLDER payload — the exact opposite of the intended last-write-wins behavior. Fix: extract the merge into a pure helper (merge.ts) with explicit first-write-wins semantics. Synthesized seeds the map first (preserves the pass-9 reservation that core- prefix wins on collision); file canvases skip ids already present, so the newest wins. Adds 5 regression tests covering: empty input, synth + file mix, duplicate-id newest-wins (the regression), synth-vs-file collision, and final sort order. Also adds a "Why no HTML canvas type?" section to the feature writeup HTML — the most-asked design question, answered honestly so reviewers don't have to ask. Covers the trust-hierarchy-inversion argument (agents are lowest-trust + LLMs are prompt-injectable + same-origin HTML = supervisor JS execution), the leaky-sanitizer argument, and the legitimate sandboxed-iframe path for v0.5+. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/canvases-feature.html | 41 +++++++++++- .../[id]/canvases/__tests__/merge.test.ts | 63 +++++++++++++++++++ .../app/api/sessions/[id]/canvases/merge.ts | 30 +++++++++ .../app/api/sessions/[id]/canvases/route.ts | 10 +-- 4 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 packages/web/src/app/api/sessions/[id]/canvases/__tests__/merge.test.ts create mode 100644 packages/web/src/app/api/sessions/[id]/canvases/merge.ts diff --git a/docs/canvases-feature.html b/docs/canvases-feature.html index 5ca3aae4d..5334a0eb3 100644 --- a/docs/canvases-feature.html +++ b/docs/canvases-feature.html @@ -214,6 +214,7 @@
  • File map
  • Try it locally
  • Roadmap
  • +
  • Why no html canvas type?
  • Deliberate non-goals
  • @@ -623,7 +624,45 @@ EOF -

    10. Deliberate non-goals

    +

    10. Why no html canvas type?

    +

    The most-asked design question — answered honestly here so reviewers don't have to ask.

    +

    Two reasons, both load-bearing.

    + +

    Trust hierarchy inversion

    +

    Agents are the lowest-trust surface in the system. They emit content on every poll cycle, and large parts of that content come from LLMs that can be prompt-injected. If an agent ingests a malicious URL, summarizes a poisoned document, or just hits a jailbreak, its next "render this nice summary" call could include attacker-controlled HTML. Putting that HTML into the same-origin dashboard means the attacker now has:

    + +

    That's the same blast radius v0.4 renderer plugins have — but renderer plugins clear npm install review once. An agent emits a fresh canvas every poll. The trust gap is enormous.

    + +

    HTML sanitization is a leaky abstraction

    +

    DOMPurify, sanitize-html, etc. are honest attempts but every year ships new bypasses — <svg> foreign objects, <math> payloads, mutation-XSS via clipboard, CSS exfiltration via attr(). A sanitizer-based HTML canvas is a 90% solution that fails open on the 10%, and the 10% is "execute arbitrary JS in your supervisor". Even GitHub, GitLab, and Slack treat HTML rendering as a multi-quarter security investment, not a feature you slot in next to markdown.

    + +

    What the existing types cover

    + + + + + + + + + + +
    You want to renderUse
    Formatted text with bold / italic / headings / code / listsmarkdown (the v0.1 parser handles all of this safely — no HTML pass-through)
    Code changesdiff
    Structured rowstable
    Metric cardsstats
    Custom UI (flame graph, Gantt, network diagram)canvas-renderer plugin (v0.4)
    Genuinely arbitrary, untrusted markupSandboxed iframe canvas — credible v0.5+ option, deferred
    + +

    The legitimate "I need raw HTML" path — sandboxed iframe

    +

    An iframe with sandbox="allow-scripts" (no allow-same-origin) runs in a null origin — it can't reach your auth tokens or call APIs. Add a postMessage protocol for size negotiation and limited event-out, document the trust trade-off explicitly, and you've got a real HTML surface that's safe by construction.

    +

    But — per codex's pass-13 advice — half-baked iframe support is worse than no sandbox. Sandbox protocols have to be designed end-to-end (CSP headers, postMessage validation, focus management, accessibility, viewport sizing) before they ship. v0.4 renderer plugins is the bigger unlock; the iframe canvas comes after, only if a real use case shows up.

    + +
    + Bottom line — not because we couldn't, but because direct HTML inverts the trust hierarchy (agents shouldn't be able to JS-execute in the supervisor) and sanitizers don't fix that. The plugin path covers most "I need a custom widget" cases at v0.4. Sandboxed iframe is the answer for genuine arbitrary HTML when we're ready to design the protocol properly. +
    + +

    11. Deliberate non-goals