mirror of
https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension.git
synced 2026-08-10 13:40:29 -06:00
Two verification passes, driving real Firefox 153.0.1 under Xvfb rather than reading docs, changed several load-bearing decisions. Added back: the keyboard shortcut now acts on selected text. activeTab adds no install-prompt line (confirmed by running Firefox's own formatPermissionStrings: ["activeTab","storage","menus"] yields msgs: []), and ext-commands.js grants activeTab before firing onCommand. Records the three failure shapes of executeScript, one of which resolves silently to [null] on parent-process about: pages, and that commands.getAll() reports a suggested_key as registered even when Firefox has silently overridden it. Rejected with evidence, in Appendix B: auto-submitting the prompt. The scheme authenticated navigation provenance when auto-submit needs text provenance, and a hostile page can smuggle ~16 KB of invisible instructions into info.selectionText. webNavigation.transitionType is not a security boundary either -- the marker is consumed on commit, so an uncommitted urlbar navigation leaves it armed for the next navigation to inherit (demonstrated at 10.47s), and its freshness guard is dead code. Prefill-only keeps the human checkpoint that makes the context menu safe. Also rejected: a bundled extension page as search_url (Firefox refuses to install), and browser.omnibox -- which works and needs no permissions, but moves the transmitter of search terms from Firefox to extension code, the change that puts data_collection_permissions ["none"] at risk. Declined to minimise AMO exposure. New Risk 6 records the residual AMO question and that it should be asked before submission, since a forced searchTerms declaration would add the install line this design exists to avoid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
809 lines
45 KiB
Markdown
809 lines
45 KiB
Markdown
# Extension Options Panel — Design
|
||
|
||
- **Date:** 2026-07-29
|
||
- **Status:** Approved for planning
|
||
- **Branch:** `feat/extension-options-panel` (cut from `develop` @ `5edb288`)
|
||
|
||
## Summary
|
||
|
||
Add an options panel to the extension, reachable from `about:addons` → the
|
||
extension's detail card → the **Options** tab (Windows) / **Preferences** tab
|
||
(macOS, Linux). The panel configures two new entry points that this change also
|
||
introduces: a context-menu item on selected text, and a keyboard shortcut that
|
||
does the same for the current selection, falling back to a fresh empty chat when
|
||
nothing is selected.
|
||
|
||
The existing `gpt <query>` address-bar search provider is **not** modified and
|
||
does not read the settings. See [Why the search provider is untouched](#why-the-search-provider-is-untouched).
|
||
|
||
New permissions: `storage`, `menus`, and `activeTab`. All three are in Firefox's
|
||
no-prompt class, so the install dialog gains no new lines and
|
||
`data_collection_permissions: ["none"]` remains truthful.
|
||
|
||
## Goals
|
||
|
||
1. Ship a working options panel embedded in `about:addons`.
|
||
2. Ship two new entry points whose behaviour the panel controls.
|
||
3. Add no permission-warning lines to the install prompt.
|
||
4. Keep `data_collection_permissions: ["none"]` accurate.
|
||
5. Introduce unit testing for the pure logic, without new runtime dependencies.
|
||
6. Never promise behaviour that ChatGPT does not reliably deliver.
|
||
|
||
## Non-goals
|
||
|
||
- Making the `gpt <query>` search provider configurable. The declarative
|
||
`search_url` is fixed at install time, and the three routes around that were
|
||
each rejected: host permissions plus navigation interception (conflicts with
|
||
goals 3 and 4), a bundled extension page as `search_url` (Firefox refuses to
|
||
install), and `browser.omnibox` (works, but moves the transmitter of search
|
||
terms from Firefox to extension code — see Risk 6). `gpt <query>` keeps
|
||
behaving exactly as it does today.
|
||
- Auto-submitting the prompt on the user's behalf. Rejected on security grounds
|
||
after dedicated adversarial testing; see
|
||
[Appendix B](#appendix-b-why-auto-submit-was-rejected).
|
||
- Moving the `gpt` keyword to `browser.omnibox`. Deliberately declined to
|
||
minimise AMO data-collection exposure, not for lack of a working mechanism.
|
||
- A toolbar button.
|
||
- Named/multiple prompt templates with add-remove-reorder UI. One template only.
|
||
- Migrating to Manifest V3.
|
||
- Chrome or other-browser packaging (`AGENTS.md` browser scope).
|
||
- User-facing error notifications. The `notifications` permission *does*
|
||
generate an install warning, so every failure path degrades silently instead.
|
||
|
||
## Background and constraints
|
||
|
||
The extension is currently manifest-only: no JavaScript exists, and the
|
||
manifest declares no `background`, `content_scripts`, or
|
||
`web_accessible_resources` key. Its single feature is
|
||
`chrome_settings_overrides.search_provider` with
|
||
`search_url: "https://chatgpt.com?q={searchTerms}"`.
|
||
|
||
### Why the search provider is untouched
|
||
|
||
`chrome_settings_overrides.search_provider` is declarative and fixed at install
|
||
time. No WebExtension API can rewrite an installed engine's URL. Making
|
||
`gpt <query>` honour the settings would require intercepting the navigation it
|
||
starts, which needs host access to `chatgpt.com` plus `webRequest` or `tabs` —
|
||
all of which add install-prompt lines. Rejected in favour of goals 3 and 4.
|
||
|
||
`is_default` stays `false`. Setting it to `true` triggers a second, separate
|
||
consent doorhanger (`webext-default-search-description`) outside any permission
|
||
list, plus an allowlist requirement.
|
||
|
||
### ChatGPT URL parameters are undocumented
|
||
|
||
None of `q`, `model`, `hints`, or `temporary-chat` is documented by OpenAI.
|
||
There is no contract and no support channel. The design is therefore resilient
|
||
by construction: the URL is built from a template with no runtime dependency on
|
||
any parameter being honoured, each parameter is independently removable so a
|
||
user can bisect when ChatGPT ignores one, and all user-facing copy is written at
|
||
hint strength. See [Appendix A](#appendix-a-verification-findings) for the
|
||
evidence behind each parameter's confidence rating.
|
||
|
||
The one parameter with quasi-official standing is `q`, because OpenAI's own
|
||
"ChatGPT search" browser extension uses `https://chatgpt.com/?q=`. Build on it;
|
||
treat everything else as best-effort.
|
||
|
||
**`q` prefills but does not submit — confirmed for this extension.** Auto-submit
|
||
was real historically but has been prefill-only since approximately November
|
||
2025, after OpenAI gated it on the `Sec-Fetch-Site` header as a prompt-injection
|
||
mitigation.
|
||
|
||
Verified on 2026-07-29 in the maintainer's own logged-in Firefox profile, on
|
||
this extension's actual navigation path: the prompt lands in the composer and
|
||
waits. The user presses Enter twice — once to leave the address bar, once to
|
||
send. The hypothesis that address-bar navigation might still be favoured
|
||
(`Sec-Fetch-Site: none` being the most-trusted value) **does not hold in
|
||
practice**.
|
||
|
||
No copy in the options panel, README, or AMO listing may say the query is
|
||
*searched*, *sent*, or *asked* — only that ChatGPT opens **with the query filled
|
||
in**. The two-keypress behaviour is an upstream ChatGPT change, not a defect in
|
||
this extension, and is documented as such rather than worked around; see
|
||
[Rejected alternatives](#rejected-alternatives).
|
||
|
||
## Architecture
|
||
|
||
Manifest stays **V2**. Mozilla has not announced any MV2 deprecation and
|
||
commits to at least 12 months' notice. The repo is MV2 today and has no Chrome
|
||
target, so migration is an orthogonal change. The code is nonetheless kept
|
||
MV3-portable: ES-module background, `persistent: false`, no `browser_style: true`.
|
||
|
||
ES modules work in an MV2 background script via
|
||
`"background": {"scripts": [...], "type": "module"}`, supported since Firefox
|
||
112 and verified empirically in Firefox 153.0.1. No background HTML shim is
|
||
needed. The options page is a normal extension page and can use
|
||
`<script type="module">` in both the embedded and standalone surfaces.
|
||
|
||
`strict_min_version` becomes `"112.0"` — the binding floor, above
|
||
`storage.sync` quota enforcement (79), `menus` (55), `options_ui` (55), and
|
||
`commands` (48).
|
||
|
||
### File layout
|
||
|
||
```
|
||
manifest.json
|
||
background/
|
||
background.js # registers menu + command, handles clicks. Imports lib/.
|
||
lib/
|
||
defaults.js # DEFAULT_SETTINGS — single source of truth
|
||
build-url.js # PURE: (settings, rawQuery) -> url string
|
||
settings.js # mergeSettings() pure; load()/save() wrap browser.storage
|
||
# load() -> storage.sync.get() piped through mergeSettings()
|
||
# save() -> storage.sync.set(), debounced by the caller
|
||
options/
|
||
options.html
|
||
options.css
|
||
options.js
|
||
test/
|
||
build-url.test.js
|
||
settings.test.js
|
||
docs/superpowers/specs/
|
||
2026-07-29-extension-options-design.md
|
||
```
|
||
|
||
`lib/build-url.js` and `lib/defaults.js` have zero browser dependencies, so
|
||
`node:test` imports them directly with no stubbing. `lib/settings.js` separates
|
||
the pure `mergeSettings()` from the `browser.storage` I/O for the same reason;
|
||
only `mergeSettings()` is unit-tested.
|
||
|
||
`lib/` ships inside the ZIP because the background script imports it at runtime.
|
||
`test/` and `docs/` must not ship — see [Packaging](#packaging).
|
||
|
||
### Manifest additions
|
||
|
||
```json
|
||
"permissions": ["storage", "menus", "activeTab"],
|
||
"background": {
|
||
"scripts": ["background/background.js"],
|
||
"type": "module",
|
||
"persistent": false
|
||
},
|
||
"options_ui": {
|
||
"page": "options/options.html",
|
||
"open_in_tab": false,
|
||
"browser_style": false
|
||
},
|
||
"commands": {
|
||
"ask-chatgpt": {
|
||
"suggested_key": { "default": "Alt+Shift+G" },
|
||
"description": "Ask ChatGPT about the selected text"
|
||
}
|
||
}
|
||
```
|
||
|
||
`browser_specific_settings.gecko` gains `"strict_min_version": "112.0"`. The
|
||
existing `id` and `data_collection_permissions` are unchanged.
|
||
|
||
`browser_style: false` is set **explicitly**. Omitting it is not equivalent — it
|
||
defaults to `true` in MV2. Setting it false means the panel renders identically
|
||
if the extension ever moves to MV3, where `browser_style: true` is unsupported.
|
||
|
||
`menus` is the canonical Firefox permission name; `contextMenus` is a
|
||
cross-browser alias. Use `menus` and access the API as `browser.menus`.
|
||
|
||
`Alt+Shift+G` is deliberate. `Ctrl+Shift+G` is Firefox's Find Previous — it
|
||
would validate, display in `about:addons`, and never fire.
|
||
|
||
## Settings
|
||
|
||
Stored as individual keys in `browser.storage.sync`.
|
||
|
||
| Key | Type | Default | Maps to |
|
||
|---|---|---|---|
|
||
| `model` | string | `""` | `model=<value>`, omitted when empty |
|
||
| `temporaryChat` | boolean | `false` | `temporary-chat=true` when set |
|
||
| `webSearch` | boolean | `false` | `hints=search` when set |
|
||
| `openIn` | `"new-tab"` \| `"background-tab"` \| `"new-window"` | `"new-tab"` | tab/window creation |
|
||
| `promptTemplate` | string | `"{query}"` | wraps the selection |
|
||
|
||
Defaults reproduce today's behaviour exactly: an empty `model` and both
|
||
toggles off mean the only parameter sent is `q`.
|
||
|
||
### Model
|
||
|
||
A **free-text field**, not a dropdown. No authoritative slug list exists and
|
||
every published one has rotted (`gpt-4` → `o1-preview` → `gpt-4o` →
|
||
`gpt-5-thinking` → `auto`/`instant`/`thinking`). A curated dropdown would be
|
||
wrong within a quarter, and each stale entry fails *silently* to the account
|
||
default.
|
||
|
||
- Label: **Model (experimental)**
|
||
- Placeholder: `auto`
|
||
- Help text: "ChatGPT may ignore this and use your account default. Leave blank
|
||
to always use your account default."
|
||
- Default is empty, not `auto`, so the default configuration sends no `model`
|
||
parameter and matches current behaviour.
|
||
|
||
### Temporary chat
|
||
|
||
- Label: **Request a temporary chat**
|
||
- Help text: "ChatGPT may not honour this. Check ChatGPT's own indicator before
|
||
sending anything sensitive."
|
||
- Default **off**.
|
||
- The words *private*, *incognito*, and *secure* must not appear near this
|
||
setting. Reports indicate chats can revert to permanent after the first
|
||
message, and OpenAI is actively reworking the feature's semantics.
|
||
|
||
### Web search
|
||
|
||
- Label: **Ask ChatGPT to use web search (hint only)**
|
||
- Help text: "A hint to ChatGPT's interface, not a guarantee."
|
||
- Default **off**. The value is hard-coded to `search`; no mode dropdown.
|
||
|
||
### Open in
|
||
|
||
- Label: **Open in**
|
||
- Options: `A new tab` / `A new background tab` / `A new window`
|
||
- Default `A new tab`.
|
||
|
||
### Prompt template
|
||
|
||
- Label: **Prompt template**
|
||
- Default `{query}`
|
||
- Help text: "`{query}` is replaced with your selected text."
|
||
|
||
### Panel behaviour
|
||
|
||
Auto-save on change with a transient "Saved" indicator; no Save button. Writes
|
||
are debounced at 400 ms so typing in a text field cannot approach any
|
||
write-rate quota.
|
||
|
||
A **Restore defaults** button resets every key to `DEFAULT_SETTINGS`.
|
||
|
||
The panel subscribes to `browser.storage.sync.onChanged` and updates its
|
||
controls live, so a remote sync landing while the panel is open does not get
|
||
clobbered by stale field values.
|
||
|
||
The embedded surface inside `about:addons` is narrow and inherits that page's
|
||
layout. The panel must therefore be responsive down to roughly 400 px, must not
|
||
depend on window sizing, and must remain usable as a standalone page — the
|
||
`extensions.htmlaboutaddons.inline-options.enabled` pref can degrade it to a
|
||
tab. Because `browser_style` is off, the panel is hand-styled and **must
|
||
support dark theme via `prefers-color-scheme`**; `about:addons` follows the
|
||
system theme.
|
||
|
||
## URL construction
|
||
|
||
`buildChatGptUrl(settings, rawQuery) -> string` in `lib/build-url.js`. Pure.
|
||
|
||
```
|
||
BASE = "https://chatgpt.com/"
|
||
MAX_URL_CHARS = 8000
|
||
```
|
||
|
||
The root path is mandatory. `chatgpt.com/search?q=` began returning 404 in
|
||
mid-2025; the root form is what OpenAI's own extension uses.
|
||
|
||
### Algorithm
|
||
|
||
1. **Build the prompt.** Let `q = String(rawQuery ?? "").trim()`.
|
||
- If `q === ""` → `prompt = ""` (the template is ignored entirely).
|
||
- Else if the template contains `{query}` → replace **every** occurrence
|
||
(`replaceAll`).
|
||
- Else if the trimmed template is empty → `prompt = q`.
|
||
- Else → `prompt = trimmedTemplate + " " + q` (template treated as a prefix;
|
||
the selection is never silently discarded).
|
||
|
||
2. **Measure the non-`q` budget.** Build a `URL` from `BASE`, append `model`,
|
||
`temporary-chat`, and `hints` per the rules below, and record
|
||
`baseLength = url.href.length`.
|
||
|
||
3. **Fit the prompt.** If `prompt !== ""`, find the largest number of *code
|
||
points* of `prompt` (via binary search over `Array.from(prompt)`, so
|
||
surrogate pairs are never split) whose resulting full URL length is
|
||
`<= MAX_URL_CHARS`. Truncation operates on the decoded string and encoding
|
||
happens afterwards, so a percent-escape can never be severed. If the kept
|
||
text contains whitespace within its final 10%, cut at the last such
|
||
whitespace instead, for a cleaner break.
|
||
|
||
4. **Assemble in final order.** Build a fresh `URL` and set parameters in this
|
||
order, matching the most-cited working examples: `q` (if non-empty), then
|
||
`model`, `temporary-chat`, `hints`.
|
||
|
||
Measuring in step 2 without `q` and assembling with `q` first is safe: total
|
||
serialized length is independent of parameter order, since reordering only
|
||
swaps which separator is `?` and which are `&`, never how many there are.
|
||
|
||
5. Return `url.href`.
|
||
|
||
### Parameter rules
|
||
|
||
- `q` — set only when the fitted prompt is non-empty. Never emit `q=` with an
|
||
empty value.
|
||
- `model` — set to `settings.model.trim()` only when that is non-empty.
|
||
- `temporary-chat` — set to the string `"true"` only when `temporaryChat` is true.
|
||
- `hints` — set to the string `"search"` only when `webSearch` is true.
|
||
|
||
`q` is the parameter to send. ChatGPT canonicalises it to `prompt=` internally;
|
||
`prompt=` is the first fallback to try should `q` ever stop working. Never send
|
||
both.
|
||
|
||
## Data flow
|
||
|
||
1. **Trigger.** Two entry points, both producing a query string:
|
||
- `browser.menus.onClicked` supplies `info.selectionText` directly. No
|
||
injection, no `activeTab` involved.
|
||
- `browser.commands.onCommand` reads the selection itself: resolve the active
|
||
tab with `tabs.query({active: true, currentWindow: true})`, then
|
||
`tabs.executeScript` returning `window.getSelection().toString()`. Read
|
||
**only** `tab.id` from the query result — reading `tab.url` or `tab.title`
|
||
would require the `tabs` permission, which *does* carry an install warning.
|
||
|
||
Pass **both** `allFrames: true` (a selection inside an iframe is invisible
|
||
to the top frame) and `matchAboutBlank: true` (needed for `about:blank`
|
||
frames). Have the injected snippet report which frame it came from, and
|
||
take the first non-empty result.
|
||
|
||
**Null-check every element of the result array.** `executeScript` does not
|
||
only throw — on parent-process `about:` pages such as `about:addons` and
|
||
`about:preferences` it resolves *silently* to `[null]`, so
|
||
`results[0].text` is a TypeError waiting to happen. If nothing usable comes
|
||
back the query is empty and a blank chat opens.
|
||
2. `settings = await load()` — `load()` already pipes the raw
|
||
`storage.sync.get()` result through `mergeSettings()`, so callers always
|
||
receive a complete, validated settings object and never need to merge again.
|
||
3. `url = buildChatGptUrl(settings, query)`.
|
||
4. Open per `settings.openIn`:
|
||
- `new-tab` → `browser.tabs.create({ url, active: true })`
|
||
- `background-tab` → `browser.tabs.create({ url, active: false })`
|
||
- `new-window` → `browser.windows.create({ url })`
|
||
|
||
`tabs.create` and `windows.create` require no permissions. The created `Tab`
|
||
object's `url`/`title` must not be read — that *would* require the `tabs`
|
||
permission, which carries an install warning.
|
||
|
||
The menu item is registered with `contexts: ["selection"]`, so Firefox shows it
|
||
only when text is selected.
|
||
|
||
## Error handling
|
||
|
||
Every path resolves to something useful. No path shows an error to the user.
|
||
|
||
| Case | Behaviour |
|
||
|---|---|
|
||
| Selection empty or whitespace | Omit `q` entirely; open a blank chat with the other settings applied. |
|
||
| `executeScript` fails on a restricted page (`about:`, `addons.mozilla.org`, `accounts.firefox.com`, `view-source:`, the PDF viewer) | Failure has **three** measured shapes, all of which must be handled: a thrown `Missing host permission for the tab`, a thrown variant naming frames, and a **silent resolution to `[null]`** on parent-process `about:` pages. Catch the throws, null-check the array, and treat all three as an empty selection → open a blank chat. The shortcut must never appear broken just because the user pressed it on a Firefox page. |
|
||
| Prompt exceeds the URL budget | Truncate per step 3 above. Never sever a percent-escape or split a surrogate pair. |
|
||
| `storage.sync` rejects (quota, or a sanitized backend error) | `try`/`catch` → fall back to `DEFAULT_SETTINGS`. Never block opening ChatGPT. Do not branch on error type: Firefox sanitizes all non-quota backend errors to `"An unexpected error occurred"`. |
|
||
| Fresh or signed-out profile returns `{}` | `mergeSettings` layers stored values over `DEFAULT_SETTINGS`, so `{}` yields defaults. |
|
||
| Template lacks `{query}` | Treat as a prefix and append. |
|
||
| Template has several `{query}` | Replace all occurrences. |
|
||
| `model` blank or whitespace | Omit the parameter. |
|
||
| Stored settings corrupt or partial | `mergeSettings` drops unknown keys and falls back to default on wrong type or out-of-range enum. |
|
||
| Background event page wakes and re-registers | `menus.removeAll()` before `menus.create()`, so re-registration is idempotent. |
|
||
| `windows.create` fails | Fall back to `tabs.create`. |
|
||
| `browser.commands` undefined | Guard before subscribing. The API does not exist on Firefox for Android. |
|
||
|
||
`storage.sync` needs no capability probe: it works with no Mozilla account,
|
||
silently behaving as local storage, and syncs later if the user signs in and has
|
||
Add-ons sync enabled. The `gecko.id` (already present) is required and **must
|
||
never change** — synced data is keyed by extension ID.
|
||
|
||
## Testing
|
||
|
||
### Unit — `node --test`
|
||
|
||
`test/build-url.test.js`:
|
||
- Defaults + a simple query → exactly `https://chatgpt.com/?q=hello`
|
||
- Each parameter appears only when its setting is enabled
|
||
- All four URL-affecting settings together (`model`, `temporaryChat`,
|
||
`webSearch`, `promptTemplate`), with parameters in the specified order.
|
||
`openIn` never influences the URL and must not appear in these assertions.
|
||
- Encoding of `&`, `#`, `?`, `=`, `+`, spaces, newlines, emoji, CJK
|
||
- `{query}` substitution: present, absent (prefix fallback), multiple occurrences,
|
||
empty template
|
||
- Empty / whitespace-only query → no `q` parameter at all
|
||
- Truncation: result `<= MAX_URL_CHARS`; no severed percent-escape; no split
|
||
surrogate pair (assert with an emoji at the boundary); whitespace-boundary
|
||
preference
|
||
- `model` empty and whitespace-only → no `model` key
|
||
|
||
`test/settings.test.js` — `mergeSettings` with: `{}`, `null`, `undefined`,
|
||
missing keys, unknown keys, wrong types, out-of-range `openIn` value.
|
||
|
||
### Manual
|
||
|
||
Retain the three existing regression checks from `AGENTS.md`: `gpt <query>`
|
||
opens ChatGPT, the provider stays non-default, and it remains configurable in
|
||
Firefox search settings.
|
||
|
||
Add:
|
||
- The Options/Preferences tab appears on the extension's detail card in
|
||
`about:addons`, and all five settings controls plus **Restore defaults**
|
||
render correctly — **in both light and dark theme**, and at a narrow width
|
||
- The page is also usable standalone via `browser.runtime.openOptionsPage()`
|
||
- Settings persist across a browser restart
|
||
- The context-menu item appears only when text is selected, and is absent otherwise
|
||
- `Alt+Shift+G` with text selected opens ChatGPT with that text, matching the
|
||
context menu; with nothing selected it opens a blank chat; on a restricted
|
||
page (`about:config`, an AMO page) it opens a blank chat rather than failing
|
||
visibly. It appears under `about:addons` → gear → Manage Extension Shortcuts
|
||
and can be rebound
|
||
- **The shortcut must be verified by actually pressing it.**
|
||
`commands.getAll()` reports a `suggested_key` as registered even when Firefox
|
||
has silently overridden it — measured: `Ctrl+Shift+Y` (Downloads on Linux)
|
||
showed as registered and never fired. A green `getAll()` is not evidence the
|
||
shortcut works.
|
||
- A selection inside an iframe is picked up by the shortcut, not just top-level
|
||
page text
|
||
- Each of the three `openIn` values behaves correctly
|
||
- **A packaged build's install prompt gains no new lines.** This cannot be
|
||
checked with `npm run dev` — install prompts are suppressed for temporary
|
||
installs. Use a packaged build.
|
||
- `unzip -l web-ext-artifacts/*.zip` shows no `test/` or `docs/` entries, not
|
||
even empty directory entries
|
||
|
||
### Parameter reality check
|
||
|
||
One pass in a real, logged-in Firefox profile to record which parameters are
|
||
actually honoured on this extension's navigation path: does `q` prefill only or
|
||
also submit; is `model` respected alongside `q`; does `temporary-chat` produce a
|
||
temporary chat and does it survive the first message; does `hints=search` engage
|
||
web search.
|
||
|
||
No further desk research substitutes for this. **The outcome adjusts the help
|
||
text, not the architecture** — every label is already written at hint strength,
|
||
so a negative result requires no redesign.
|
||
|
||
## Packaging
|
||
|
||
`web-ext-config.mjs` — add to `ignoreFiles`:
|
||
|
||
```js
|
||
"docs", "docs/**", "test", "test/**",
|
||
```
|
||
|
||
Both forms per directory are required. A glob alone removes the files but leaves
|
||
an **empty directory entry** in the ZIP; web-ext's own code pairs the bare name
|
||
with the glob for exactly this reason.
|
||
|
||
`.gitea/` and `.agents/` need **no** entries — web-ext ignores all dotfiles and
|
||
dot-directories by default. `web-ext-artifacts/**` is likewise already redundant;
|
||
it stays as-is to keep this diff focused.
|
||
|
||
`web-ext lint` scans `lib/` and `test/` with the **same** file filter as
|
||
`build`. With `lint.warningsAsErrors: true`, one addons-linter warning in a test
|
||
file is a hard CI failure. Adding `test/` to `ignoreFiles` fixes linting and
|
||
packaging together — it is one lever, not two.
|
||
|
||
`web-ext-config.mjs` must keep its `.mjs` extension. web-ext rejects a `.js`
|
||
config file outright.
|
||
|
||
## Tooling
|
||
|
||
`package.json`:
|
||
|
||
```jsonc
|
||
"type": "module", // NEW — required, see below
|
||
"scripts": {
|
||
"test:unit": "node --test", // NEW
|
||
"test": "npm run lint && npm run test:unit && npm run build"
|
||
}
|
||
```
|
||
|
||
`"type": "module"` is **required**, not stylistic. Without it, `node --test`
|
||
importing an ES-module `lib/*.js` relies on Node's module-syntax detection,
|
||
which:
|
||
|
||
- emits a permanent `MODULE_TYPELESS_PACKAGE_JSON` warning on every run, and
|
||
- **fails outright on Node 22.0–22.6**, since detection only became default in
|
||
22.7. `engines.node` says `>=22` and CI pins the bare major `"22"`, leaving
|
||
the patch to runner resolution — so without this field the project declares
|
||
support for a range in which its own suite does not run.
|
||
|
||
Using `.mjs` for test files only does **not** fix this; the typeless
|
||
`lib/*.js` is the ambiguous file. Adding `"type": "module"` was verified not to
|
||
break `web-ext lint` or `web-ext build` on web-ext 10.5.0, which never consults
|
||
the consumer package's `type` field.
|
||
|
||
Ordering is lint → unit → build, so unit failures prevent a ZIP from being
|
||
produced. The existing `.gitea` workflow calls `npm test` and needs no edit.
|
||
|
||
`node --test` **exits 0 when it discovers zero test files.** CI would go green
|
||
on a misconfigured path, so acceptance must assert a non-zero test count, not
|
||
merely a zero exit code.
|
||
|
||
Note for implementers: local Node here is v26.5.0 while CI pins 22. Local green
|
||
does not imply CI green.
|
||
|
||
## Documentation changes
|
||
|
||
- **README** — an Options section listing the five settings and how to reach the
|
||
panel (referring to "Options (Windows) / Preferences (macOS, Linux)", since
|
||
Firefox's label is platform-dependent). Privacy section amended: settings are
|
||
stored in the user's own browser profile and, only if that user has Firefox
|
||
Sync with Add-ons enabled, synced end-to-end encrypted through their own
|
||
Mozilla account. Do **not** write "never leaves your device" unconditionally.
|
||
Nothing reaches Zavage Software, so the zero-data-collection claim stands.
|
||
|
||
Revisit the bare sentence "This extension collects zero user data." It is true
|
||
in the plain-English sense and `data_collection_permissions: ["none"]` is
|
||
retained, but AMO's definition of transmission is broader (Policy 6, 6.2.2.1),
|
||
and the context menu does put selected text into a chatgpt.com URL. Prefer
|
||
wording that is specific about what happens — the extension sends the text you
|
||
chose to the ChatGPT URL you asked it to open, stores your settings in your own
|
||
profile, and reports nothing to Zavage Software — over a blanket claim a
|
||
reviewer could read differently. See Risk 6.
|
||
- **README** — document the two-keypress behaviour plainly as an upstream
|
||
ChatGPT limitation: ChatGPT stopped auto-submitting prompts passed by URL, so
|
||
the query arrives typed into the composer and the user presses Enter to send.
|
||
State that this is outside the extension's control and that no API key,
|
||
server, or intermediary is involved. Do not describe it as a bug or promise a
|
||
fix.
|
||
- **AGENTS.md** — update Project Structure for the new directories, Build/Test
|
||
commands for `test:unit`, and the Testing Guidelines manual checklist.
|
||
- **CHANGELOG.md** — entry for the release.
|
||
- **Version** — `manifest.json` `1.0` → `1.1`; `package.json` `1.0.0` → `1.1.0`.
|
||
|
||
## Rejected alternatives
|
||
|
||
| Alternative | Why rejected |
|
||
|---|---|
|
||
| Rewrite the `gpt` keyword's navigation to honour settings | Needs host access to `chatgpt.com` plus `webRequest`/`tabs`; adds install-prompt lines. |
|
||
| Point `search_url` at a bundled extension page that redirects | Firefox requires an http(s) search URL; also a visible redirect flash. Not pursued. |
|
||
| ~~Keyboard shortcut reads the page selection~~ | **No longer rejected — now in scope.** Initially dropped believing `activeTab` carried a permission cost. Verification showed `activeTab` is in Firefox's `OptionalPermissionNoPrompt` class and adds no install warning, and reading a selection on an explicit gesture to build the requested URL transmits nothing, so `data_collection_permissions: ["none"]` holds. The lighter-looking alternatives are all worse: a declared `<all_urls>` content script is a real host permission with a warning. |
|
||
| `background.page` HTML shim for ES modules | Works, but unnecessary once `background.type: "module"` was confirmed for MV2. |
|
||
| `background.scripts` without `type` | Classic scripts; an `import` statement fails with a compile-time `SyntaxError` and silently kills the script. |
|
||
| Curated model dropdown | Slug lists rot within a quarter and every stale entry fails silently to the account default. |
|
||
| Migrate to MV3 now | Orthogonal, larger diff. Mozilla has announced no MV2 sunset and promises ≥12 months' notice. |
|
||
| Toast/notification on failure | The `notifications` permission adds an install warning. All failure paths degrade silently instead. |
|
||
| Content script on `chatgpt.com` that presses Enter for the user | **Rejected on security grounds after dedicated adversarial testing — see [Appendix B](#appendix-b-why-auto-submit-was-rejected). Not a future path; the minimum bar is recorded there so it cannot be revived cheaply.** Three independent attack lenses returned "not viable". The decisive result is that the design authenticates the wrong thing: proving the extension started the navigation is easy, but auto-submit needs to prove the *user authored the prompt*, and a hostile page can smuggle ~16 KB of invisible text into `info.selectionText`. **Decision: document the two-keypress behaviour as an upstream ChatGPT limitation instead.** |
|
||
| Opt-in auto-submit via `optional_permissions` | The permission machinery works — verified, `permissions.request()` succeeds from the inline `about:addons` panel and the Firefox install dialog is provably unchanged. It was the *safety* that failed, not the plumbing. Also: an optional host permission is visible on the AMO listing page before install, so it was never free. |
|
||
| `browser.omnibox` to make `gpt <query>` honour the settings | **Technically viable and verified working** — needs no permissions, reuses the same `gpt` keyword, lints clean. Rejected for policy exposure: it moves the transmitter of search terms from Firefox to extension code, which is the specific change that puts `data_collection_permissions: ["none"]` at risk (AMO 6.2.2.1 classes search terms as personal data). Declining it keeps Firefox as the transmitter. Revisit only after AMO answers the declaration question. |
|
||
| `search_url` pointing at a bundled extension page | Impossible, not merely discouraged. The Firefox schema and addons-linter both constrain `search_url` to `https://` or `http://localhost`, and Firefox 153.0.1 **refuses to install the extension entirely** ("Extension is invalid") for `moz-extension://`, a relative path, or a root-relative path. `search_url_post_params` only changes the method; `search_form` is scheme-restricted and unsupported on Firefox. |
|
||
|
||
## Risks
|
||
|
||
1. **Any ChatGPT parameter may stop working without notice.** Mitigated by
|
||
hint-strength copy everywhere, independently removable parameters, and no
|
||
architectural dependence on any of them. The `/search` path's silent move to
|
||
404 in mid-2025 is the precedent.
|
||
2. **`model` may never work alongside `q`.** An unresolved report says `q`
|
||
forces the account default; no 2026 source positively confirms otherwise. The
|
||
setting is labelled experimental and defaults to empty, so the default
|
||
configuration is unaffected either way.
|
||
3. **`Alt+Shift+G` may collide with an OS or window-manager hotkey.** Verified
|
||
clear of Firefox's own keyset and DevTools only; no registry of other add-ons'
|
||
shortcuts exists. Mitigated by the context menu remaining a full-featured
|
||
path and by documenting Manage Extension Shortcuts.
|
||
4. **MV2's long-term future is not guaranteed.** The strongest Mozilla
|
||
commitment is dated March 2024 and nothing newer restates it. Mitigated by
|
||
keeping the code MV3-portable.
|
||
5. ~~The shortcut's selection-reading depends on a `commands` invocation
|
||
conferring `activeTab`.~~ **Resolved, positive.** Firefox's
|
||
`ext-commands.js` calls `tabManager.addActiveTabPermission(tabTracker.activeTab)`
|
||
before firing the event, and a real synthesized keypress in Firefox 153.0.1
|
||
read page text with `activeTab` as the only host-ish permission. No longer a
|
||
risk.
|
||
6. **`data_collection_permissions: ["none"]` carries residual policy risk for
|
||
the selection paths.** AMO Policy 6 defines transmission as data handled
|
||
outside the add-on or local browser, and 6.2.2.1 names search terms as
|
||
personal data. Once extension code reads page text and puts it in a
|
||
chatgpt.com URL, a reviewer could argue `websiteContent` applies. The
|
||
mitigating reading is 6.2.2.2's implicit consent — a user-initiated action
|
||
through a clearly labelled control — but 6.2.2.2(5) is explicitly
|
||
discretionary and no published Mozilla guidance resolves it. This design
|
||
deliberately minimises exposure by **not** moving the `gpt` keyword to
|
||
`browser.omnibox`, leaving Firefox as the transmitter for the address-bar
|
||
path. If AMO ever requires declaring `searchTerms`, the install prompt gains
|
||
"The developer says this extension collects: search terms" — which would
|
||
conflict with this design's no-new-install-lines goal. Worth asking AMO
|
||
before submission, not at submission.
|
||
|
||
## Appendix A: Verification findings
|
||
|
||
Established 2026-07-29 by parallel research with adversarial verification;
|
||
64 findings, one overturned by its skeptic. Browser claims were verified by
|
||
loading real test extensions into **Firefox 153.0.1**. Repo claims were verified
|
||
by running commands against copies under `/tmp`; the repo itself was unmodified.
|
||
|
||
### Confirmed empirically in Firefox 153.0.1
|
||
|
||
- MV2 `background: {scripts, type: "module"}` loads real ES modules. Supported
|
||
since Firefox 112 (`bug 1811443`); Firefox's manifest schema puts no
|
||
`min_manifest_version` on `type`.
|
||
- MV2 `background: {scripts}` **without** `type` fails on `import` with
|
||
`"SyntaxError: import declarations may only appear at top level of a module"`.
|
||
- `options_ui.open_in_tab: false` renders inside `about:addons`
|
||
(`inline-options-browser`), gated on
|
||
`extensions.htmlaboutaddons.inline-options.enabled`, which ships `true`.
|
||
- The tab label is platform-dependent:
|
||
`preferences-addon-button = { PLATFORM() -> [windows] Options *[other] Preferences }`.
|
||
- Options pages support `<script type="module">` in both surfaces.
|
||
|
||
### Confirmed from Firefox source
|
||
|
||
- `PERMISSIONS_WITH_MESSAGE` (26 entries) is the authoritative list of API
|
||
permissions that add a warning line. `storage`, `menus`, and `activeTab` are
|
||
all absent; `tabs` is present. **Caveat established by the overturned finding:**
|
||
it is one of five independent install-prompt inputs (API permissions, host
|
||
permissions, `DATA_COLLECTION_PERMISSIONS`, site permissions, unsigned
|
||
warning), so it is not a complete account of install-time warnings.
|
||
- A fresh install **always** shows a prompt; the no-prompt early-return is gated
|
||
on `type == "update"`. This extension's prompt today is "Add …" + the
|
||
data-collection line + the private-windows checkbox — unchanged by this work.
|
||
- Install prompts are suppressed for temporary/unpacked installs.
|
||
- `storage.sync` throws only for a *temporary* add-on ID
|
||
(`enforceNoTemporaryAddon`); the explicit `gecko.id` satisfies it. No account
|
||
check exists anywhere in the path — the backing store is a plain profile-local
|
||
`storage-sync-v2.sqlite`. Quotas: 102400 total / 8192 per item / 512 items.
|
||
Non-quota backend errors are sanitized to `"An unexpected error occurred"`.
|
||
- Sync transport is per-extension encrypted BSOs over the user's own Firefox
|
||
Account; extension IDs are hidden behind random GUIDs.
|
||
- `Ctrl+G` is `key_findAgain` and `Ctrl+Shift+G` is `key_findPrevious`.
|
||
`Alt+Shift+G` is unused by `browser-sets.inc` and DevTools.
|
||
- `tabs.create` / `windows.create` declare no permissions; only
|
||
`Tab.url`/`title`/`favIconUrl` require `tabs`.
|
||
- `commands` is `version_added: false` on Firefox for Android.
|
||
- `options_ui.browser_style` is deprecated in **MV3 only** (default flipped in
|
||
Firefox 115, unsupported in 118). Legal in MV2, where it defaults to `true`.
|
||
|
||
### Confirmed empirically against the repo
|
||
|
||
- `npx web-ext lint` → 0 errors / 0 warnings / 0 notices, exit 0. Green baseline.
|
||
- lint and build share one `FileFilter`; `eval()` planted in `lib/` produced
|
||
`DANGEROUS_EVAL` and exit 1.
|
||
- web-ext's default ignore list includes `'**/.*'` and `'**/.*/**/*'`, so
|
||
dotfiles never ship. Confirmed: `.gitea/` and a planted `.agents/note.md` were
|
||
both absent from a build with no dotfile entries in `ignoreFiles`.
|
||
- `test/` and `docs/` **do** ship by default. With only `"test/**"`, empty
|
||
`test/` and `docs/` directory entries remained in the ZIP; adding the bare
|
||
names removed them.
|
||
- The current shipped ZIP contains only `manifest.json`, `LICENSE.txt`, and
|
||
`icons/` — packaging hygiene is a property to preserve, not a bug to fix.
|
||
- `node --test` with no `"type"` field: passes on Node 26.5.0 but warns
|
||
`MODULE_TYPELESS_PACKAGE_JSON`; with `--no-experimental-detect-module` (pre-22.7
|
||
behaviour) it **fails** with `"Cannot use import statement outside a module"`.
|
||
- `.mjs` for tests only moves the warning to the typeless `lib/*.js`.
|
||
- Adding `"type": "module"`: `node --test` clean, `web-ext lint` 0/0/0,
|
||
`web-ext build` succeeded.
|
||
- `node --test` with zero discovered tests: `tests 0`, exit **0**.
|
||
- Local Node v26.5.0, npm 12.0.1, web-ext 10.5.0. CI pins node-version `"22"`.
|
||
|
||
### ChatGPT parameters — confidence ratings
|
||
|
||
| Parameter | Rating | Basis |
|
||
|---|---|---|
|
||
| `q` prefills | **Solid** | Dated 2026 sources; maintained tooling; OpenAI's own extension uses it; a live probe returned HTTP 200 with all parameters preserved. |
|
||
| `q` auto-submits | **Refuted, and confirmed by first-hand test** | Prefill-only in every dated report since 2025-11-11. Tenable TRA-2025-22 documents OpenAI gating auto-submit on `Sec-Fetch-Site`. Reproduced 2026-07-29 in the maintainer's logged-in Firefox via this extension's own search-provider path. |
|
||
| `model` honoured alongside `q` | **Uncertain** | An unresolved 2024-12-28 report says `q` forces GPT-4o regardless; counter-evidence exists from 2024–2025; no 2026 source positively confirms. Maintained tooling grades it experimental. |
|
||
| Valid `model` slugs | **Uncertain** | No authoritative list; sightings rot (`gpt-4`, `o1-preview`, `gpt-4o`, `gpt-5-thinking`, `auto`, `instant`, `thinking`). |
|
||
| `temporary-chat=true` | **Uncertain** | Community-confirmed originally; multiple reports of it not applying or reverting after the first message; OpenAI reworking the feature. |
|
||
| `hints=search` is a hint only | **Confirmed** | Multiple independent sources; "a frontend user interface hint, not a backend trigger". |
|
||
| `hints=search` still functions | **Uncertain** | Freshest positive source is provably recycling stale material; freshest negative is 2025-11-11. Costs nothing to append and degrades to a normal chat. |
|
||
| Stable set of `hints` values | **Refuted** | Published value sets contradict each other; only `search` has cross-source corroboration. |
|
||
| Officially documented by OpenAI | **Refuted** | No documentation for any parameter; primary community threads have zero staff replies. |
|
||
| `/search` path | **Refuted** | Began returning 404 mid-2025. |
|
||
| `q` canonicalises to `prompt=` | **Confirmed** | Dated 2026 reports; `prompt=` is the fallback if `q` dies. |
|
||
| Combining all four | **Structurally safe** | Nothing errors or gets stripped. But "the URL loads" is not "the parameter was honoured". |
|
||
|
||
### Unresolved
|
||
|
||
- ~~Whether Firefox search-provider navigations land in the favoured
|
||
`Sec-Fetch-Site: none` bucket for auto-submit.~~ **Resolved 2026-07-29,
|
||
negative.** Tested in the maintainer's logged-in Firefox on this extension's
|
||
own path: the prompt only prefills. Being in the most-trusted navigation
|
||
bucket does not restore auto-submit.
|
||
- Whether Firefox enforces Chrome-style `storage.sync` write-rate quotas. The
|
||
constants exist in the schema but no rate-limiting logic appears in the Rust
|
||
backend. Made moot by debouncing writes.
|
||
- Whether `Alt+Shift+G` collides with OS-level or other-add-on shortcuts.
|
||
- Whether AMO accepts `required: ["none"]` once extension code reads page text
|
||
(Risk 6). Ask AMO; 6.2.2.2(5) is discretionary and unresolved in published
|
||
guidance.
|
||
|
||
### Round two (2026-07-30)
|
||
|
||
A second verification pass, again with adversarial challenge. Browser claims
|
||
tested by driving real Firefox 153.0.1 under Xvfb via Marionette — real
|
||
keypresses, real address-bar input, real permission doorhangers — not headless
|
||
approximations.
|
||
|
||
- **A `commands` keyboard shortcut confers `activeTab`.** Firefox's
|
||
`ext-commands.js` calls `tabManager.addActiveTabPermission(tabTracker.activeTab)`
|
||
before firing `onCommand`. A synthesized real keypress read page text with
|
||
`permissions: ["activeTab", "menus"]` and nothing else; the identical call
|
||
from a no-gesture background path threw `Missing host permission for the tab`.
|
||
- **`executeScript` fails in three shapes**, one silent: two distinct throws plus
|
||
a resolution to `[null]` on parent-process `about:` pages.
|
||
- **`suggested_key` loses silently to Firefox's built-ins** while
|
||
`commands.getAll()` still reports it registered. `Ctrl+Shift+Y` never fired.
|
||
- **`activeTab` adds no install-prompt line** — re-confirmed by running
|
||
Firefox's own `ExtensionData.formatPermissionStrings` inside 153.0.1:
|
||
`["activeTab", "storage", "menus"]` yields `msgs: []`, byte-identical to
|
||
requesting nothing.
|
||
- **`search_url` cannot be an extension page.** Schema and addons-linter
|
||
constrain it to `https://` or `http://localhost`; Firefox 153.0.1 refuses to
|
||
install ("Extension is invalid") for `moz-extension://`, relative, and
|
||
root-relative paths.
|
||
- **`browser.omnibox` works with zero permissions** and can reuse the `gpt`
|
||
keyword alongside the registered engine, linting clean. Declined for policy
|
||
exposure, not capability — see [Rejected alternatives](#rejected-alternatives).
|
||
- **`permissions.request()` works from the inline `about:addons` options page.**
|
||
Bug 1382953 was fixed in Firefox 61. The grant does not reload the inline
|
||
page. It must be the **first statement** in the click handler: awaiting a real
|
||
API call first loses the gesture and rejects with `permissions.request may
|
||
only be called from a user input handler`, and the gesture does not survive
|
||
`runtime.sendMessage` delegation to the background.
|
||
- **An optional permission cannot reach the Firefox install dialog** — the
|
||
install prompt's info object has no top-level `optionalPermissions`, the only
|
||
key the string formatter reads for that section. Verified with real XPI
|
||
installs plus an order-reversed control.
|
||
- **But the AMO listing page does change.** With an optional host permission the
|
||
listing gains "Optional permissions: Access your data for chatgpt.com",
|
||
*before* install. Confirmed against a live MV2 control (SponsorBlock). Note
|
||
publishing the already-committed `data_collection_permissions` will surface
|
||
that card by itself, independent of anything here.
|
||
|
||
## Appendix B: Why auto-submit was rejected
|
||
|
||
Recorded in full so the feature cannot be revived without re-clearing the same
|
||
bar. Three adversarial lenses — prompt injection, permission integrity, AMO
|
||
policy — independently returned "not viable".
|
||
|
||
**The design authenticated the wrong invariant.** "Did the user initiate this
|
||
navigation?" is provable. "Did the user *author this text*?" is what auto-submit
|
||
needs. For the two entry points this spec builds, the text comes from the page
|
||
DOM — so extension-initiated provenance is not merely insufficient, it is
|
||
inverted: the paths the extension can prove it started are exactly the paths
|
||
where a hostile page supplies the payload.
|
||
|
||
**Selection smuggling (confirmed empirically).** A hostile page can place ~16 KB
|
||
of invisible instructions into `menus.onClicked`'s `info.selectionText` while the
|
||
user sees only innocuous bait text. The user's click is genuine; the prompt is
|
||
the attacker's. The only thing standing between an attacker-chosen prompt and
|
||
the user's logged-in ChatGPT account is that the text sits visibly in the
|
||
composer until a human presses Enter. **Auto-submit deletes that checkpoint.**
|
||
This is also why the context menu and shortcut remain safe *as long as they only
|
||
prefill*.
|
||
|
||
**`webNavigation.transitionType` is not a security boundary.** The signal looks
|
||
right — a `gpt` search emits `transitionType: "generated"` with
|
||
`transitionQualifiers: ["from_address_bar"]`, and content cannot forge those
|
||
values directly. But the marker is stashed per-tab and consumed only when a
|
||
document *commits*, so a urlbar navigation that never commits (HTTP 204, a
|
||
download, an external-protocol handler, a cancelled load) leaves it armed and
|
||
**the next navigation to commit inherits it, whoever started it.** Demonstrated:
|
||
a page navigating itself via its own inline timer was reported as `typed` +
|
||
`["from_address_bar"]` **10.47 seconds** after the urlbar action, against a
|
||
control of `link` + `[]`. The intended 5-second freshness guard is dead code —
|
||
`data.time - Date.now() > RECENT_DATA_THRESHOLD` is always false (inverted
|
||
subtraction, and a microsecond-scaled constant compared against a millisecond
|
||
delta) — so the stale marker never expires and an attacker need win no race: it
|
||
arms a timer on blur and waits. Separately, `webNavigation` needs no host
|
||
permission to see full URLs, so granting it would expose the user's entire
|
||
real-time browsing history for a gate that does not hold.
|
||
|
||
**Same-tab substitution defeats the pending-record scheme (confirmed).** Even
|
||
restricted to omnibox-typed text, `omnibox`'s default disposition is the current
|
||
tab. The outgoing hostile document receives `beforeunload` — a deterministic
|
||
signal that the extension's navigation has begun, not a race — arms
|
||
`setTimeout(…, 0)`, and sets `location.href` to `chatgpt.com/?q=EVIL`. Measured
|
||
beacon sequence: `ext-navigate-start[q=HONEST]` → `beforeunload fired` →
|
||
`timer0: set location OK` → `chat-loaded[q=EVIL-TIMER]`. The extension believes
|
||
it navigated the tab to the user's prompt; the document actually there is the
|
||
attacker's, and the pending record is still armed. (Opener-based substitution is
|
||
dead in Firefox 153 — chatgpt.com sends `COOP: same-origin`, and cross-origin
|
||
location writes are denied regardless.)
|
||
|
||
**What was *not* the problem.** The plumbing all works: `permissions.request()`
|
||
from the inline panel, the unchanged Firefox install dialog, optional
|
||
data-collection permissions in Firefox 153. The rejection is about safety, and
|
||
secondarily about AMO policy — not about feasibility.
|
||
|
||
**Minimum bar if ever revisited.** Auto-submit may only ever apply to text the
|
||
extension can attest the user typed into browser chrome (an `omnibox`
|
||
`onInputEntered` value — a page can neither write the omnibox, fire the event,
|
||
nor observe it). Never to `info.selectionText`. It must open a **new** tab, never
|
||
navigate the current one. The pending map must stay in memory only — never
|
||
persisted, because tab ids restart low at browser start, creating a replay
|
||
hazard. Authorization must be re-validated synchronously at the instant of
|
||
submit against the live `location.search`, never latched, since chatgpt.com is a
|
||
client-routed SPA and the content script survives same-document navigation.
|
||
Single attempt, ~1.5–2 s timeout, `allFrames: false` with an explicit top-frame
|
||
assertion, abandon permanently on any navigation event, never retry, never a
|
||
fallback chain. And the AMO declaration question (Risk 6) must be answered
|
||
first, because it decides whether the feature is compatible with a clean install
|
||
prompt at all.
|