fix(dashboard): let a frame re-issue its own sandbox-doc GET (#6176) - #6184
fix(dashboard): let a frame re-issue its own sandbox-doc GET (#6176)#6184bolichen97 wants to merge 1 commit into
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Backend fix with no new UI surface; it removes a silent blank-frame failure, and the one string touched is a doc comment made accurate. Watch
[UX-REVIEWED] 330b149 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsServed entries within their grace window are still classified evictable, so gallery pressure can re-blank the exact frame this PR set out to fix. FINDING — src/kiro_crew/dashboard/handlers/sandbox_doc.py:112 — a serve collapses the entry to [OPUS-REVIEWED] 330b149 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — 🔴 changes requested (blocking)GPT 5.6 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/dashboard/handlers/sandbox_doc.py:230 -- replay grace makes the bearer URL reusable behind shared proxies |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS The 10-second grace only rescues re-issues that follow within milliseconds; two of the three triggers the fix names can arrive minutes later and still blank silently. Watch
[DESIGN-REVIEWED] 330b149 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The fix is derived and minimal, but the new comments promise served entries a grace-window eviction shield the unchanged inequality mathematically never provides. What this change shipsIntent: stop desktop-app widgets/artifacts rendering permanently blank when a frame re-issues the GET for its already-spent sandbox-doc URL — a FIX (#6176).
No new config key, flag, or exported surface; the fix reuses WatchThe description says "a served entry becomes evictable", yet the shipped comments say the opposite: "Within this window an entry must not be evicted … and a URL already served may be served again" ( Subtractions
[FIRST-PRINCIPLES-REVIEWED] 330b149 |
A widget or artifact frame that re-issues the GET it just made — renderer recovery, restore from tray, history navigation, all of which Electron does — found the document already popped from the stash. The 404 lands INSIDE the frame, where nothing in the frontend observes it, and the frame stays at `opacity: 0` until `onLoad` fires: a permanently blank area with no error. The first successful GET now collapses the entry's deadline to `_IN_FLIGHT_GRACE_SECS` instead of popping it, under the same lock, with `min` so a replay cannot push the deadline out. Single use stays the load-bearing control — `request.remote` is the proxy's address behind a tunnel or reverse proxy, so the client binding is worth nothing exactly where a leaked URL is most reachable — and the replay window is seconds rather than the 15-minute mint TTL. Drops the Electron `frame-ancestors` strip from the first revision: the mint returns a RELATIVE url and all four consumers assign it straight to `iframe src`, and the desktop window loads the dashboard top-level at `http://localhost:<port>`, so frame and parent origins are identical by construction and `frame-ancestors 'self'` has no reachable trigger. That also removes the new module the packaging allowlist test was failing on.
1999840 to
330b149
Compare
|
🤖 Holding this PR rather than pushing a third revision. GPT 5.6's re-block is correct, and while checking whether a narrower design could satisfy it I falsified the causal chain in my own revision-2 description. Both halves of this PR are now unsupported as fixes for #6176. 1. The 2. The single-use half does not produce the reported symptom either. Revision 2 claimed a spent-URL re-fetch renders as a permanently blank area because the 404 lands inside the frame and the iframe stays at An iframe navigation that receives a 404 with a body is a completed navigation, so 3. There is no design that satisfies both constraints. GPT's threat model is that model-authored HTML reads its own URL, exfiltrates it, and a co-tenant replays it through the shared proxy address. Distinguishing "the same frame re-loading" from "another client holding the same URL" needs something the attacker lacks, and behind a proxy there is nothing: the client address is shared, session cookies are foreclosed (the module docstring records that a sandboxed frame in some engines does not send one — that is why this route is on the auth bypass list), and Since the re-fetch tolerance buys no confirmed fix for #6176, paying a security relaxation for it is the wrong trade. Not overriding. What #6176 actually needs: the root cause is still unidentified. The symptom — a frame that loads and paints nothing, no error text, on two independent surfaces, desktop-only — points away from the transport (both halves here) and toward the document rendering at zero effective size or with nothing painted. The next honest step is instrumentation on a desktop build (does |
|
Closing this superseded implementation rather than resolving its conflict. The hold analysis established that the proposed replay window does not explain #6176 and weakens the single-use sandbox URL boundary. Safer document-load recovery work has since landed in #6461 and #6481. Keeping #6176 open for reporter verification and further desktop instrumentation; this branch is not deleted. |
Pull request was closed
Summary
Fixes #6176 — generated widgets and HTML artifacts render blank in the desktop app while working fine in a browser.
Revision 2 rewrites this PR. The first revision claimed two root causes and shipped a fix for each; one of them was wrong. Both blocking reviews were correct and the diff is now 3 files instead of 5.
Root cause
The sandbox-doc handler
_stash.pop()'d the entry on the first GET, so the URL was permanently spent. A frame that re-issues the GET it just made — renderer recovery, restore from tray, back/forward navigation, all of which Electron does — got a 404. That 404 lands inside the frame, where nothing in the frontend observes it (the document is served with an opaque origin, so the parent cannot read whether it loaded), and the frame stays atopacity: 0untilonLoadfires. The result is a permanently blank area with no error and no retry affordance.Changes
src/kiro_crew/dashboard/handlers/sandbox_doc.py_IN_FLIGHT_GRACE_SECSinstead of popping it — same lock,min()so a replay cannot push the deadline out._evictablerewritten to the equivalent form that reads honestly for both a fresh mint and a served entry (same inequality, no behaviour change).test/test_sandbox_doc_channel.pywebsite/src/hooks/useSandboxDoc.tsretrydoc comment, which asserted the URL was single-use with no grace.Responding to the blocking reviews
GPT 5.6 — "replayable token bypasses client binding behind shared proxies" (BLOCKING). Correct, and the first revision made it worse by deleting the paragraph that documented exactly this:
request.remoteis the proxy's address whenever the dashboard is reached through a tunnel or reverse proxy, so every client shares it and the binding is worth nothing precisely where a leaked URL is most reachable. A plain_stash.get()therefore left a 15-minute replay window for any co-tenant behind that proxy.A bare
pop()cannot be restored, because it is the cause of the bug this PR closes. Session-cookie auth is foreclosed by design — the module docstring records that a sandboxed frame in some engines does not send one, which is why the route is on the auth bypass list at all. So the resolution is to keep the spend and bound the window: serving collapses the deadline to seconds (_IN_FLIGHT_GRACE_SECS, 10s), long enough for a renderer to re-issue a GET that follows its predecessor by milliseconds, 90× shorter than the mint TTL the plain read left in place. The reasoning is restored to the docstring rather than removed, and pinned by a source-level test.First Principles — "the
frame-ancestorsstrip has no reachable cause" (BLOCK). Correct. Verified independently: the mint returns a relative url (sandbox_doc.py), all four consumers (WidgetFrame,ArtifactBody,ArtifactsPage,RemoteArtifactDetailPage) assign it straight toiframe src, and the desktop window loads the dashboard top-level athttp://localhost:<port>(main.jsBACKEND_URL). A relative src resolves against the parent's own origin, so frame and parent origins are identical by construction andframe-ancestors 'self'matches whatever the host spelling — there is nolocalhostvs127.0.0.1producer in this tree.website/electron/sandbox-doc-headers.js, its test file, and theonHeadersReceivedblock are all deleted, which is also what the failing Electron Shell Tests check was reporting (Missing from build.files: sandbox-doc-headers.js).The now-false sentence in
useSandboxDoc.tsis fixed as well.Not in scope
First Principles' second Watch item —
_evictablecap-evicting an entry ~10s after mint, 404ing a legitimate late re-fetch — is pre-existing on main and independent of this change. It is left alone here so this diff stays the size of the defect it closes.Security
Content-Security-Policy: sandboxon the response is untouched, and no CSP is now rewritten anywhere in the Electron shell — model-authored HTML still gets an opaque origin._MAX_ENTRIES,_MAX_BYTES) unchanged; a served entry becomes evictable, so the first revision's addedstash_fullpressure is gone too.Testing
test/test_sandbox_doc_channel.py— 42 passedwebsite/electronshell-contract.test.js— 5 passed (was failing on the deleted module)tsc --noEmit, eslint — all cleanmain@24cb5e76c