feat: runtime offscreen polyfill (fixes OneNote-class breakage) - #9
Merged
Conversation
Firefox has no chrome.offscreen and its runtime.getContexts rejects the OFFSCREEN_DOCUMENT context type, so OneNote-class extensions that call these APIs unguarded throw and die on startup after conversion. Adds a conditionally-injected shim (shims/offscreen-polyfill.js) that emulates the offscreen document as a hidden iframe and patches getContexts to report it. Unlike the existing always-on shims, this one only fires when the manifest declares the "offscreen" permission or a packaged .js file references chrome.offscreen/OFFSCREEN_DOCUMENT, via a shared should_inject_offscreen_polyfill() predicate used by both the shim generator and the manifest background.scripts wiring (which lists it before the extension's own scripts). offscreen_converter.rs (the static, per-usage offscreen conversion path) is untouched.
generate_shims() and the conversion-report note both gated purely on should_inject_offscreen_polyfill() (the trigger), while transform_background() only wires shims/offscreen-polyfill.js into background.scripts when a background section exists. A triggering extension with no background got an orphaned shim file plus a misleading "injected" report note. Adds will_inject_offscreen_polyfill() = trigger && background.is_some() as the single source of truth for all three call sites (shim generator, manifest wiring, report note), so they can't drift again.
Adds a minimal test extension (aliased, unguarded getContexts + createDocument, no feature detection) reproducing the OneNote failure shape as a non-quarantined e2e corpus entry. Chrome runs real offscreen documents; Firefox runs the converted build through Task 1's polyfill; equivalence is asserted via the storage.local.set offscreenResult write, verified to match on both sides (not just a clean trace diff). allowed_diffs widened beyond the original offscreen.* pattern to also cover runtime.getContexts* and a pinned runtime.getURL call, both consequences of the same spy-shim-before-polyfill injection order (issue #6) one layer deeper than expected -- documented in _notes, no shim code changed.
Local Firefox auto-updated 152.0.6 -> 153.0.1, pulling in geckodriver 0.37.1 which blocks WebDriver navigation to internal schemes (moz-extension:, about:, chrome:) from content context. Launching the geckodriver service with --allow-system-access restores popup navigation without needing a chrome-context switch (which turns out to be a dead end: navigateTo is content-context-only regardless).
The polyfill unblocks the original getContexts crash (#2): Firefox now creates the offscreen document and getContexts reports it present. But OneNote's offscreen-communicator relay hits a second, independent bug one layer deeper -- its onMessage listener is an async function that also calls sendResponse synchronously, and Firefox's native chrome.* compat surface discards that response in favor of the async function's own (always-undefined) implicit promise resolution, crashing on JSON.parse(undefined). Filed as #8, with the same "OneNote's own bug, not a chrome2moz mis-conversion" reasoning as #2. Since OneNote still gates its whole post-install setup behind the same unhandled-rejecting promise chain, the same downstream patterns (getManifest, tour tab, contextMenus, etc.) remain chrome-only for a different reason than before. Deleted the four pre-polyfill cascade patterns and re-derived allowed_diffs from the current traces: re-added offscreen.*/runtime.getContexts*/runtime.getURL#offscreen.html as issue-#6 tracing-blind-spot allowances (same style as the offscreen-gate entry), added runtime.sendMessage#offscreen and runtime.error#JSON.parse for #8's crash and its now-unreached downstream calls, and rewrote every remaining note to point at the new root cause. Entry stays quarantined (issue #5, no fixture host). pnpm e2e --only gojbdfnpnhogfdgjbigejoaolejmgdhk: PASS, 0 unallowed divergences (Firefox 7 background events, Chrome 36 -- was 8 vs 36 pre-polyfill; see #2 comment for full before/after).
Review on PR #9 found the 8->7 Firefox event-count writeup had the crash-point direction backwards: issue #8's crash happens LATER in OneNote's actual control flow than the old getContexts throw (getContexts succeeds, createDocument succeeds, the offscreen document loads, the first relay message sends -- none of that ran before). The lower raw count is an issue-#6 tracing-blind-spot artifact (getContexts/createDocument now run through the polyfill's untraced surface), not less progress. Rewrote corpus.json's _offscreen_relay_root_cause note and the task report to state this explicitly, and to stop treating raw event count as a progress metric on this entry. Marked the pre-polyfill 8-event trace as not re-derivable (results/ is gitignored, overwritten by this session's own runs) instead of implying it's known. Added a decisive, isolated regression-control probe to the offscreen-gate synthetic extension: a second onMessage listener matching OneNote's exact shape (async function, manual synchronous sendResponse, no return true), on its own message kind/storage key so it can't touch the already-passing baseline probe. Confirmed deterministically (2 runs): Chrome's sendMessage resolves to the real sendResponse value, Firefox's resolves to undefined, discarding it -- proving the issue #8 theory decisively rather than asserting it from code inspection alone. The resulting divergence is allowlisted with four tight, content-pinned patterns and a note stating this is an intentional, permanent regression gate to delete once chrome2moz ships a compat fix -- not a tracing artifact to quietly work around. Also: pinned OneNote's runtime.getURL allowance to its actual observed (normalized <ext>/... ) trace form instead of the looser unnormalized one that happened to still match; added a side-agnostic-masking caveat to the getContexts* note; corrected "reaches the offscreen document round-trip successfully" to state the response leg fails. cargo test: 70 passed, 1 ignored (was mis-reported as "15+12" off a truncated tail). pnpm e2e --only offscreen-gate: PASS, 0 unallowed divergences (now includes the issue-#8 regression-control probe). Full pnpm e2e: exit 0, all 3 entries PASS. Issue #2 comment edited in place to mark the Chrome baseline as not re-measured pre-polyfill and add the likely pingProbe-fix explanation for the ~42->36 figure. Issue #8 comment added with the decisive repro plus two precision corrections (multi-listener response-channel caveat; Chrome doesn't "fall back" to the promise, it ignores it once sendResponse fired).
getContexts/createDocument/closeDocument/hasDocument now accept an optional trailing callback (Chrome's classic calling convention) alongside the existing Promise form. Also: an explicit empty contextTypes filter now matches everything (native semantics) instead of matching nothing; a disconnected iframe (window.close()/DOM teardown) is treated as no document and self-heals on the next call; createDocument rejects non-extension-origin URLs, matching Chrome's own guard.
…tion Add a third offscreen-gate probe exercising hasDocument/closeDocument via callback form, then recreate + parse round-trip -- Chrome (native) and Firefox (polyfill) store byte-identical results, matched with no new allowed_diffs entry needed. Spec's Verification section now reflects the shipped OneNote measurement (8->7 traced events while executing strictly further, not "8 toward 42"; root cause is issue #8, proven by the gate's regression control) instead of the draft's speculative framing, documents the background-section injection precondition, and adds two known behavioral out-of-scope limits (suspension non-survival, window.close() self-teardown no-op) plus a pointer to follow-up issue #10 for the remaining known polyfill gaps.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a runtime
chrome.offscreenpolyfill for Firefox, closing the gapdescribed in #2: extensions that call
chrome.offscreen.createDocument/chrome.runtime.getContextswithout feature-detecting them crashsynchronously on Firefox (
chrome.runtime.ContextType.OFFSCREEN_DOCUMENTisundefinedthere, and Firefox's parameter validation throws on it), whichcommonly cascades into killing an extension's entire post-install setup when
that call sits behind an unguarded
.then()chain -- exactly OneNote WebClipper's shape.
Root cause → polyfill → gate → measurement
Root cause (Extensions using chrome.offscreen without feature-detection break entirely on Firefox (e.g. OneNote Web Clipper) #2): OneNote's background script calls
chrome.runtime.getContexts({contextTypes:[chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], ...})with no guard. Firefox has no offscreen-document context type at all, so
this throws synchronously, and the throw propagates through an
unhandled-rejecting promise chain (
clipperIdProcessed) that gatesregisterContextMenuItems(),onInstalled(), and the actual clip-triggerpath -- none of it ever runs on Firefox.
Polyfill (
shims/offscreen-polyfill.js, injected only when abackground section exists and the extension actually references
chrome.offscreen/getContextswith an offscreen context type):chrome.runtime.ContextType.OFFSCREEN_DOCUMENTas a realstring, removing the Firefox validation throw.
runtime.getContextsto report a synthetic offscreen-documentcontext once one has been created.
chrome.offscreen.createDocument/closeDocument/hasDocument, backed by a hidden iframe in the background page (theconverted background is an event page with a DOM, so this works).
Differential gate (
e2e/testdata/offscreen-extension, corpus entryoffscreen-gate): a synthetic extension exercising the samegetContexts+createDocument+sendMessagepattern OneNote uses, with nofeature-detection guard. Passes with identical stored results on both
browsers, confirming the polyfill is functionally correct end-to-end,
not just trace-clean. (The
offscreen.*/runtime.getContexts*/runtime.getURL#offscreen.htmlallowances on this entry are a pre-existingtracing limitation, Spy shim injected before converter compat shims (shims/*.js), hiding converter-added API surface from the trace diff #6 -- the harness's spy shim runs before the
converter's own compat shims, so polyfill-internal calls are untraced on
Firefox. Documented in the entry's
_notes.)OneNote measurement: re-triaged
e2e/corpus.json's OneNote entryagainst the polyfill. Firefox's traced background-event count went from
8 to 7 -- but that drop is NOT a regression: Firefox now demonstrably
executes further into OneNote's code (
getContextssucceeds,createDocumentsucceeds, the offscreen document loads, the first relaymessage is sent -- none of that ran before). The lower raw count is a
side effect of issue Spy shim injected before converter compat shims (shims/*.js), hiding converter-added API surface from the trace diff #6 (those steps run through the polyfill's untraced
surface). What actually stops it now is a second, independent bug in
OneNote's offscreen-relay messaging -- filed as chrome.runtime.onMessage: async listener that calls sendResponse synchronously has its response discarded on Firefox (exposed by OneNote once the offscreen polyfill unblocks it) #8, with a decisive,
deterministic reproduction: an
asynconMessagelistener that alsocalls
sendResponsesynchronously has its response silently discarded byFirefox's native
chrome.*compat surface, unlike Chrome. OneNote isstill non-functional end-to-end on Firefox, but the specific failure
mode Extensions using chrome.offscreen without feature-detection break entirely on Firefox (e.g. OneNote Web Clipper) #2 reports (unguarded offscreen API usage killing everything) is
fixed for extensions in this shape generally, per the gate's clean pass.
Full numbers and allowlist rationale: see the comment on Extensions using chrome.offscreen without feature-detection break entirely on Firefox (e.g. OneNote Web Clipper) #2 and
e2e/corpus.json's updated_notes. Entry staysquarantined: true(blocked separately on E2E harness backlog: kill/wake probe, alarms fast-forward, monkey crawler + LLM judge, CI watchlist #5, no fixture host).
Issue chrome.runtime.onMessage: async listener that calls sendResponse synchronously has its response discarded on Firefox (exposed by OneNote once the offscreen polyfill unblocks it) #8 regression control: added a second, isolated
onMessagelistener to the
offscreen-gateextension, matching OneNote's exactasync-listener/manual-
sendResponseshape on its own message kind, toturn the chrome.runtime.onMessage: async listener that calls sendResponse synchronously has its response discarded on Firefox (exposed by OneNote once the offscreen polyfill unblocks it) #8 theory into a decisive, reproducible probe rather than an
inference from code reading alone. Confirmed (2 consecutive runs): Chrome
resolves
sendMessageto the realsendResponsevalue; Firefox resolvesit to
undefined. This divergence is intentionally allowlisted (tight,content-pinned patterns) and documented as a permanent regression gate to
delete once a compat fix ships -- not something quietly worked around.
Test plan
cargo test-- 71 passed, 1 ignored (network-dependent), 0 failedcd e2e && pnpm typecheck-- cleancd e2e && pnpm test-- 53 passed, 2 skipped, 0 failedcd e2e && pnpm e2e(full, headless), run twice to confirmdeterminism -- exit 0 both times, all three corpus entries PASS with
0 unallowed divergences (LatexToCalc, OneNote Web Clipper
(quarantined), C2M Offscreen Gate incl. the issue-chrome.runtime.onMessage: async listener that calls sendResponse synchronously has its response discarded on Firefox (exposed by OneNote once the offscreen polyfill unblocks it) #8
regression-control probe and the new callback-form probe below)
Final fix wave (this update)
getContexts,createDocument,closeDocument,hasDocumentnow accept an optional trailing callback(Chrome's classic calling convention) in addition to the existing Promise
form --
cb(result)on success,cb(undefined)on failure.contextTypes: []filter now matcheseverything (native semantics), matching
!hasTypeshandling instead ofmatching nothing; a disconnected iframe (
window.close()/DOM teardown) isnow treated as "no document" and self-heals on the next
createDocument/hasDocument/getContextscall instead of staying wedgedon a stale reference;
createDocumentnow rejects if the resolved URLisn't an extension-origin page, matching Chrome's own guard.
e2e/testdata/offscreen-extension/background.js):exercises
hasDocument/closeDocumentvia the callback form, thenrecreates the document and repeats the parse round-trip. Chrome (native)
and Firefox (polyfill) store byte-identical results
(
offscreenCallbackResult) -- confirmed via trace inspection, matchedautomatically with no new
allowed_diffsentry required.docs/superpowers/specs/2026-07-31-offscreen-polyfill-design.md):Verification section now reflects the shipped OneNote measurement (8→7
traced events while executing strictly further, not "8 toward 42"; Chrome
traces 36 this session) and the actual crash root cause (issue chrome.runtime.onMessage: async listener that calls sendResponse synchronously has its response discarded on Firefox (exposed by OneNote once the offscreen polyfill unblocks it) #8, proven
by the gate's regression control), replacing the earlier draft's
speculative framing. Documents the background-section injection
precondition and two known behavioral limits (offscreen doc doesn't
survive event-page suspension;
window.close()self-teardown is a no-op).gaps intentionally left out of this wave (constant
contextIdacrossrecreate cycles, iframe
error-listener outliving its promise,createDocumentresolving instead of rejecting for a missing page).Refs #2, #6, #8, #10.