Skip to content

Undefined behavior when multiple extensions inject window.inference #17

Description

@SamSamskies

The spec exposes a single window.inference object. It does not say what happens if more than one extension (or a browser-native injector plus an extension) implements IPA on the same page.

request has no extension id, picker, or discovery step. The page always calls whoever currently owns that name.

What happens today

Chrome injects MAIN-world content scripts into the same JS environment. There is no IPA coordinator. Injection order across extensions is not a stable, documented picker (it often follows install / extension-id order, but apps cannot rely on that).

So window.inference.request(...) is whichever injector won that page load.

Inference Bridge already takes a side: skip if window.inference exists, then define the property as non-writable and non-configurable (content/inject.js):

if (window.inference) return;
// ...
Object.defineProperty(window, "inference", {
  value: Object.freeze({ request, /* ... */ }),
  writable: false,
  configurable: false,
  enumerable: true,
});
Who injects first Result
Bridge Bridge wins for that load. Later IPA extensions cannot overwrite it.
Another IPA extension Bridge bails. That other extension’s request is what the page calls — including its permission UI, keys, models, and getFeatures.

The user never sees a “which IPA extension?” prompt. They just get one implementation’s popup (or none, if the winner is broken). Two copies of Bridge (Chrome Web Store + unpacked) have the same fight.

Why it matters

  • Wrong keys / wrong models. Origin grants live in the winning extension, not in “IPA.”
  • Wrong capabilities. getFeatures().toolCalling is whatever the winner advertises.
  • Confusing UX. The site looks the same; the permission sheet is a different product.
  • Hostile first-writer. A malicious or sloppy extension that injects first owns the name. Bridge’s freeze only protects after Bridge has already won.

This is the collision NIP-07 never solved for window.nostr, and that Ethereum wallets hit with window.ethereum before EIP-6963 (announce-and-discover instead of one global).

What not to do (yet)

Do not make apps pick an extension on every request. That fights “applications request inference, not providers.” A multiplexer also does not share one permission grant across extensions — each still has its own keys and UI.

Possible postures

Experimental draft (document reality): the result is undefined if more than one injector is present. Tell users to run a single IPA extension.

Implementation convention (Bridge already does this): first successful injector wins; do not overwrite; freeze the object. Stops last-write chaos; still a lottery.

If this needs to survive multiple products: an EIP-6963-style announce (each extension fires a discovery event with its inference object). Keep window.inference as a default for simple apps, and define how that default is chosen (first announcer, or a user default stored by the browser / a host extension). True user choice probably wants browser UI, like a default password manager — not something pages can do securely.

Proposal for this draft

  1. Add this to README Open Questions.
  2. In SPEC.md, state that if more than one injector is present, which implementation window.inference refers to is undefined. Applications still feature-detect the object they got; they must not assume a particular extension.
  3. Optionally recommend the Bridge convention (do not overwrite; freeze) as non-normative implementation guidance.

A full discovery protocol can wait until a second real IPA extension exists and this is no longer hypothetical.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions