Skip to content

fix: close three gaps the artifact-frame review found - #6481

Merged
buluoray merged 1 commit into
mainfrom
fix/artifact-frame-followups
Aug 28, 2026
Merged

fix: close three gaps the artifact-frame review found#6481
buluoray merged 1 commit into
mainfrom
fix/artifact-frame-followups

Conversation

@buluoray

Copy link
Copy Markdown
Contributor

Follow-ups to #6461. Each of these is an internal contradiction that change introduced, found by its own review round after merge — not a new capability. Six files, no new surface.

The dead-frame retry never fired in the case it exists for

#6461 added a grace window: a frame whose document never reports its height is showing something this surface did not build (the document URL is single-use, so an engine-initiated renavigation lands on a 404 that fires load like any other navigation), and the reader gets the existing retry.

It was armed only when no document had reported yet, and that flag reset only when a new URL landed. So once a document rendered and reported, a renavigation of the same spent URL — the back/forward-cache eviction case artifacts.md explicitly claims is covered — bumped the load counter, hit the guard, and left a 404 inside the frame with no notice and no retry. Code and spec disagreed, and the disagreement hid the whole feature.

The window now re-arms on every load. A link inside an artifact navigating the frame arms it too, and that is the right outcome rather than a cost: in both cases the frame has stopped showing the artifact, and offering the retry is milder than silently re-minting over an action the reader took.

The dashboard shell was the one ancestor source nothing validated

Three sites build a frame-ancestors value from _extra_frame_ancestors. Two route through origin.frame_ancestors_value, which re-validates every entry against a strict origin form; the shell CSP hand-joined 'self' with the extras, so the helper's own guarantee — that no future ancestor source can reintroduce an inexpressible entry — did not hold for the shell.

Not a hypothetical class: http://[::1]:<port> was being emitted before #6461. A CSP host-source admits only letters, digits and hyphens, so an engine that sees a bracketed IPv6 literal refuses the whole source expression and drops every ancestor with it — which is why that grant never worked and why it produced a console warning per framed response.

The new test asserts this by behaviour, not by reading the source: with _extra_frame_ancestors stubbed to offer both an inexpressible and an expressible entry, the header must carry the expressible one and 'self' while the IPv6 literal never appears. That distinguishes a filter from a blanket refusal.

Two docstrings documented a reverted design, and a disproved mechanism

sandbox_doc.py and webapp_preview.py both said the header names the request's own origin rather than 'self', because 'self' would resolve against the response's opaque origin and match no ancestor at all.

Both halves are wrong. A probe with frame-ancestors 'self' loaded fine at single-level nesting in Chrome 152, so the opaque-origin explanation is disproved; the real cause of the original blanking is that the directive is matched against every ancestor, which 'self' alone cannot satisfy at nesting depth. And the server-derived-origin design those paragraphs describe was reverted before #6461 merged — it named http://localhost:<port> while a phone was on https://<tunnel-host> and blanked every frame on that path. The code three hunks below each docstring emits 'self', and four tests pin exactly that.

A stale docstring that restates a falsified mechanism is worse than none: it is what a future reader would reason from.

Tests

Both new guards mutation-verified — each mutation turns exactly its own target red, suite green when restored:

mutation result
shell CSP back to " ".join(["'self'", *extra]) 1 red
drop the per-load reset of the report flag 1 red

Backend 211 passed across the four touched modules; isort / flake8 / mypy / black / docs-lint clean. tsc clean, eslint 0 errors (2 pre-existing warnings in untouched branches of the same file), 37 frontend cases passed across the touched files, npm run build clean.

Why no screenshot: no UI is added or changed. Two of the three fixes are a response header and two docstrings; the third makes an already-shipped failure notice fire in one more case, using the same markup and copy #6461 already carries evidence for. The one behaviour a screenshot could show — the retry appearing after an engine-initiated renavigation — cannot be provoked in a headless Chromium run, since it depends on the engine deciding to evict and reload the frame; it is pinned by the test instead.

Follow-ups to #6461, each an internal contradiction that change
introduced rather than a new capability.

The dead-frame retry never fired in the case it exists for. The grace
window was armed only when no document had reported yet, and that flag
reset only when a NEW url landed — so once a document rendered and
reported its height, an engine-initiated renavigation of the same
single-use url (a back/forward-cache eviction, the case the spec names)
bumped the load counter, hit the guard, and left a 404 inside the frame
with no notice and no retry. The window now re-arms on every load. A
link inside an artifact navigating the frame arms it too, and that is
the right outcome: in both cases the frame has stopped showing the
artifact, and offering the retry is milder than silently re-minting over
an action the reader took.

The dashboard shell was the one ancestor-emitting site nothing
validated. Two of the three built their `frame-ancestors` through
`origin.frame_ancestors_value`; the shell hand-joined `'self'` with the
extras, so the helper's guarantee that no future ancestor can
reintroduce an inexpressible entry did not actually hold there. That
class of bug is not hypothetical — a bracketed IPv6 literal was being
emitted, and an engine that sees one refuses the whole source expression
and drops every ancestor with it.

Two module docstrings documented a design that was reverted before
merge, and worse, restated a mechanism this work disproved: they said
the header names the request's own origin because `'self'` resolves
against the response's opaque origin and matches no ancestor. A probe
showed `'self'` matching fine at single-level nesting; the real cause
was that `frame-ancestors` is matched against EVERY ancestor. The code
three hunks below each docstring emits `'self'`, and four tests pin it.

Both new guards are mutation-verified: reverting the shell to a
hand-join and dropping the per-load reset each turn exactly their own
test red.
@buluoray
buluoray requested a review from a team August 28, 2026 05:33
@buluoray
buluoray requested a review from a team as a code owner August 28, 2026 05:33
@buluoray
buluoray requested a review from CrysisDeu August 28, 2026 05:33
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of f902941d3663c92d835512b7710446e411ca9ec7 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] f902941

False positive or not applicable? A repository writer can comment:
/ai-review override gpt f902941d3663c92d835512b7710446e411ca9ec7: <one-sentence reason>

@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of f902941d3663c92d835512b7710446e411ca9ec7 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Three real internal contradictions from #6461, each fixed at its root cause — one validated ancestor builder, per-load re-arm — with mutation-verified tests.

[DESIGN-REVIEWED] f902941

@github-actions

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of f902941d3663c92d835512b7710446e411ca9ec7 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified. The review is complete; here is the structured output the contract requires.

First-Principles-Verdict: PASS

Three declared fixes, each aimed at a nameable cause, zero new surface — code brought back into line with spec, validator, and shipped behavior.

What this change ships

Intent: make the artifact-frame retry fire in the eviction case the spec already promises, and stop the shell CSP from emitting ancestors browsers refuse — a FIX.

  1. Retry notice now appears when the engine renavigates an already-rendered artifact — justified (docs/system-specs/modules/artifacts.md:541-550 documents exactly this; code disagreed).
  2. Retry notice can now also appear after an in-artifact link navigates the frame — declared; derived, since the sandboxed (opaque-origin) frame makes the two loads indistinguishable from the parent.
  3. Shell frame-ancestors now filtered by the shared validator; the bracketed IPv6 literal that made engines drop the whole directive is gone — justified, cause-level.
  4. Two docstrings no longer assert a disproved opaque-origin mechanism and a reverted design — justified (spec-and-code-in-same-commit invariant).

Item 1 sits at cause level: the observation was keyed on the URL when the invariant is per-document, and the fix re-keys it per load rather than special-casing eviction. Item 3 removes the class, not the instance: grepped frame_ancestors_value — 3 emitting sites (server.py:965, webapp_preview.py:302, sandbox_doc.py:251), all now route through the one validator; zero unfixed siblings (the base-stack.yaml CloudFront value is static config, not built from _extra_frame_ancestors). No new config key, flag, event, or exported symbol anywhere in the diff; description's "no new surface" claim holds.

[FIRST-PRINCIPLES-REVIEWED] f902941

@github-actions

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

UX-level review of f902941d3663c92d835512b7710446e411ca9ec7 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: CONCERNS

Re-arming the failure notice on every load makes a deliberate in-frame link click surface "Couldn't render this artifact" — a message that misstates what happened.

Watch

  • The error string now fires on intentional navigation. reportedRef.current = false on every onLoad means that when a user follows a link inside an artifact (the second cause the file's own comment names), the new document never reports, and after 3s a persistent strip reads "Couldn't render this artifact" over the page they chose to open — but the artifact did render, and "Retry" silently discards that page to yank them back. Before this diff a reported document suppressed the window, so this pairing is new. Low frequency (in-frame links) × moderate impact (misleading persistent banner + mismatched action) × every occurrence. Smallest fix: since the code can't distinguish the two causes, make the copy cause-neutral — e.g. "This artifact is no longer showing" with the button as "Show artifact" — instead of asserting a render failure the component didn't observe.

The bfcache-eviction fix itself (404 in the frame now gets a retry instead of nothing) and the backend header/comment changes are solid and add no UX surface.

[UX-REVIEWED] f902941

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed f902941d3663c92d835512b7710446e411ca9ec7 — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] f902941

Verdict parsed from the review's SHA-scoped output markers for commit f902941d3663c92d835512b7710446e411ca9ec7.

False positive or not applicable? A repository writer can comment:
/ai-review override fable f902941d3663c92d835512b7710446e411ca9ec7: <one-sentence reason>

@buluoray
buluoray enabled auto-merge (squash) August 28, 2026 05:44
@buluoray

Copy link
Copy Markdown
Contributor Author

The UX finding is correct, and the defect is this PR's. Verified against source rather than reasoned about: the frame is sandboxed with an opaque origin, so nothing in the component can read contentWindow.location or observe a user gesture inside the frame. The two causes of a second load — an engine-initiated renavigation of a spent URL, and a link the reader clicked — are genuinely indistinguishable from outside. Re-arming on every load is what makes the eviction case reachable at all, so it stays; but the copy then asserts a cause the component never observed, which is the actual bug.

Concretely wrong in the link case: the artifact did render, "Couldn't render this artifact" says otherwise, and "Retry" re-mints — discarding the page the reader chose to open.

The suggested remedy is the right shape. One correction to it: the neutral copy should apply only to the silence path, not to both. failed is a different observation — the mint itself failed, and there the component knows rendering did not happen, so "Couldn't render this artifact" is accurate there and should stay. That means two keys, not one renamed key:

  • failed (mint failed) → unchanged.
  • silence past the grace window → cause-neutral, e.g. "This artifact is no longer showing" with the action reading "Show artifact" rather than "Retry".

Not applied in this PR, deliberately, and this is the one judgment call worth a human: the copy lands in 14 locale files under a zero-tolerance i18n gate, and choosing the wording is a product decision rather than a mechanical fix. Everything else on this PR is green with no blocking verdict, so the choice is whether to hold it for the copy change or take it now and follow up — that is the author's call, not mine to guess.

The rest of the review's read matches mine: the bfcache-eviction fix is the point of the change, and the header/docstring corrections add no UX surface.

@buluoray

Copy link
Copy Markdown
Contributor Author

Tracked as #6489 — the cause-neutral copy for the silence path, with the two-keys-not-one correction, the 14-locale constraint, and the falsification for each existing test. Not blocking this PR: the reachable case it describes is the link-navigation one, and the notice firing there is a wrong message rather than a broken frame.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 28, 2026
@buluoray
buluoray merged commit 8b0ce87 into main Aug 28, 2026
67 checks passed
@buluoray
buluoray deleted the fix/artifact-frame-followups branch August 28, 2026 06:17
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants