From 55e581bab9b600e7c070ec596d3109fc897d2d86 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Tue, 1 Sep 2026 20:10:20 +0900 Subject: feat: Firefox selection translation extension with local Ollama (initial version) Implement the complete initial version of a Firefox WebExtension (Manifest V3) that translates selected text on web pages using a local Ollama LLM and shows the result in a small popover near the selection. Specification alignment (Phase 0) - Unified Specifications.md / Design.md / Security.md on the same approach: Ollama REST API + Fetch API + NDJSON streaming (no ollama-js SDK dependency) - Fixed host_permissions to http://localhost/* and http://127.0.0.1/* because Firefox match patterns do not support port numbers (bug 1362809/1468162); the actual connection target is restricted to localhost/127.0.0.1 + any port by application-side validation (defense in depth, OLL-09) - Clipboard: use navigator.clipboard.writeText() only inside the copy button click (user activation); no deprecated execCommand fallback Implementation (zero dependencies, vanilla JS, no npm/Node required) - manifest.json: MV3, background.scripts + type:module (event page, no service_worker), action popup, options_ui, content_scripts for http/https, permissions: storage + localhost host permissions only - src/shared: constants (ports, limits, presets, error codes), message validation, settings schema (defaults / merge / sanitize / buildOptions / toApiThinking), system prompt builder (fixed prompt first + user prompt) - src/background: message-router (runtime.Port streaming + runtime.onMessage for model list), translation-session (single active request, abort previous on new request, stale chunk suppression), ollama-client (fetch against /api/tags and /api/chat with buffering NDJSON line parser tolerant to chunk boundaries inside JSON objects, AbortController, host re-validation before every request, HTTP status to error-code mapping), settings-service (storage.local load/save with validation and fallback to defaults) - src/content/content.js: single classic script (Firefox does not support ES modules in content scripts) with selection detection (selectionchange + mouseup + keyup, debounced), Shadow DOM action button, Shadow DOM popover with HIDDEN/LOADING/STREAMING/COMPLETED/ERROR state machine, port client with requestId-based stale chunk filtering, enabled-state sync via storage.onChanged; all dynamic text rendered via textContent only - src/popup + src/options: full settings UI (enable toggle, Ollama host, model list from Ollama with saved-model warning, target language, thinking mode, temperature slider 0.0-2.0 step 0.1, context length presets 4k/8k/16k/32k/128k/256k, seed random/fixed with integer value, user system prompt, advanced numeric options with steppers, stop sequences textarea) - icons generated by tools/make-icons.ps1 (System.Drawing, no external tools) - build.ps1 packages manifest.json + icons/ + src/ into dist zip, excluding tests/, doc/, ollama-js/ and tools/ Tests (71 passed, 0 failed) - In-browser test runner (tests/test.html) with custom describe/it/assert framework; no npm, no Node.js - Unit: constants sync between content mirror and shared module, Ollama host validation (localhost/127.0.0.1 + any port only; rejects external hosts, LAN IPs, https, credentials, paths), settings schema defaults and fallback, temperature 0.1-step validation, context length presets, seed modes, advanced option types, buildOptions omission of unset values and absence of tfs_z/mirostat fields, system prompt composition order, message validation with selection length boundary values - Integration: NDJSON streaming across awkward byte boundaries, thinking chunk forwarding, malformed line tolerance, error payload mapping, think/ options inclusion rules, model list, TranslationSession abort semantics, settings round-trip with storage.onChanged cache refresh, full port flow (started/chunk/completed), newer request aborts older with stale chunk discard, malformed message rejection, OLLAMA_UNAVAILABLE and INVALID_SETTINGS error paths, untrusted sender rejection Security audit (Design.md section 24 checklist: all pass) - No innerHTML/outerHTML/document.write/eval/new Function/localStorage - No remote JS/CSS/fonts/CDN resources; no inline scripts in extension pages - Ollama access limited to background; content scripts never fetch Ollama - Background validates message type/length/requestId; single concurrent translation enforced - Clipboard API only (no execCommand fallback) - Minimal permissions: storage + localhost host permissions Docs - README.md with build/load/usage instructions, API list, permission rationale - doc/Testing-Results.md with automated test results, security audit results, and a 14-item manual Firefox verification checklist (to be executed on a real Firefox + Ollama environment) --- doc/Design.md | 871 +++++++++++++++++++++++++++++++++++++++++++++++++ doc/Security.md | 804 +++++++++++++++++++++++++++++++++++++++++++++ doc/Specifications.md | 391 ++++++++++++++++++++++ doc/Testing-Results.md | 87 +++++ 4 files changed, 2153 insertions(+) create mode 100644 doc/Design.md create mode 100644 doc/Security.md create mode 100644 doc/Specifications.md create mode 100644 doc/Testing-Results.md (limited to 'doc') diff --git a/doc/Design.md b/doc/Design.md new file mode 100644 index 0000000..20de3e5 --- /dev/null +++ b/doc/Design.md @@ -0,0 +1,871 @@ +# Design.md + +## 1. 設計概要 + +本拡張は、Webページとの接点をContent Scriptに限定し、権限を伴うOllama通信と設定管理をBackgroundへ分離する。 + +```text +Web Page + │ + │ selection + ▼ +Content Script + │ runtime.Port + ▼ +Background + │ fetch / HTTP + ▼ +Ollama + │ streaming + ▼ +Background + │ runtime.Port + ▼ +Content Script + │ + ▼ +Shadow DOM Popover +``` + +設計原則: +- ページ由来データは信頼しない。 +- Content Scriptは最小責務とする。 +- Ollama通信はBackgroundのみ(Ollama REST API + Fetch API + NDJSON streaming)。 +- UI文字列はHTMLとして評価しない。 +- 翻訳要求は常に1-shot。 +- 同時翻訳は1件に制限。 +- 設定値はUIだけでなくBackgroundでも検証する。 +- Ollama接続先は初期版ではlocalhost / 127.0.0.1 + 任意ポートのみ許可する。 + +--- + +## 2. コンポーネント構成 + +| コンポーネント | 主責務 | +|---|---| +| `content/selection-controller` | Selection検出、Range位置取得 | +| `content/action-button` | 翻訳アイコン表示/非表示 | +| `content/translation-popover` | 翻訳結果UI、stream描画、copy/regenerate/close | +| `content/port-client` | Backgroundとの長期接続 | +| `background/message-router` | Content Scriptからのrequest受信・validation | +| `background/translation-session` | requestId、AbortController、active stream管理 | +| `background/ollama-client` | Ollama REST API呼び出し(fetch + NDJSON streaming) | +| `background/settings-service` | storage読書き、default、validation | +| `popup` | 有効/無効、簡易設定、Options起動 | +| `options` | 全設定UI | +| `shared/settings-schema` | 設定型、既定値、範囲 | +| `shared/messages` | runtime message型定義 | +| `shared/constants` | 最大選択長等 | + +--- + +## 3. 推奨ディレクトリ構成 + +```text +/ +├─ manifest.json +├─ package.json +├─ src/ +│ ├─ background/ +│ │ ├─ index.js +│ │ ├─ message-router.js +│ │ ├─ translation-session.js +│ │ ├─ ollama-client.js +│ │ └─ settings-service.js +│ ├─ content/ +│ │ ├─ index.js +│ │ ├─ selection-controller.js +│ │ ├─ action-button.js +│ │ ├─ translation-popover.js +│ │ ├─ port-client.js +│ │ └─ content.css +│ ├─ popup/ +│ │ ├─ popup.html +│ │ ├─ popup.js +│ │ └─ popup.css +│ ├─ options/ +│ │ ├─ options.html +│ │ ├─ options.js +│ │ └─ options.css +│ └─ shared/ +│ ├─ settings-schema.js +│ ├─ messages.js +│ ├─ prompt.js +│ └─ constants.js +├─ tests/ +│ ├─ unit/ +│ └─ integration/ +└─ docs/ + ├─ Specifications.md + └─ Design.md +``` + +実際のビルド方式に応じて配置は調整してよいが、責務分離は維持する。 + +--- + +## 4. Manifest設計 + +概念例: + +```json +{ + "manifest_version": 3, + "name": "Selected Text Translator", + "version": "0.1.0", + "permissions": ["storage"], + "host_permissions": ["http://localhost/*", "http://127.0.0.1/*"], + "background": { + "scripts": ["background.js"], + "type": "module" + }, + "content_scripts": [ + { + "matches": ["http://*/*", "https://*/*"], + "js": ["content.js"], + "css": ["content.css"], + "run_at": "document_idle" + } + ], + "action": { + "default_popup": "popup.html" + }, + "options_ui": { + "page": "options.html", + "open_in_tab": true + } +} +``` + +### 4.1 Manifest実装ルール +- `background.service_worker` は使用しない。 +- `browser_action` は使用しない。 +- `scripting` 権限は初期版では付与しない。 +- `tabs` 権限は付与しない。 +- localhost以外のhost permissionは初期版では付与しない。 +- Firefoxのmatch patternはポート番号指定に対応していない(Firefox bug 1362809 / 1468162)。`http://localhost/*` および `http://127.0.0.1/*` は任意ポートに一致するため、実際に通信してよい接続先はアプリ側Host validatorで `http://localhost:` / `http://127.0.0.1:` のみに限定する。 + +--- + +## 5. Content Script設計 + +### 5.1 Selection取得 + +標準DOM APIを使用する。 + +```js +const selection = window.getSelection(); +const text = selection?.toString().trim(); + +if (!text) return; + +const range = selection.getRangeAt(0); +const rect = range.getBoundingClientRect(); +``` + +### 5.2 検出イベント +初期実装候補: +- `selectionchange`: 選択状態更新用 +- `mouseup`: 選択確定後の表示タイミング用 +- `keyup`: Shift + Arrow等のキーボード選択対応用 + +過剰発火を避けるため、UI更新には軽いdebounceを適用してよい。 + +### 5.3 Action Button表示条件 + +```text +selection exists +AND trimmed text length > 0 +AND text length <= MAX_SELECTION_LENGTH +AND extension enabled +``` + +### 5.4 位置計算 +- `Range.getBoundingClientRect()` を基本とする。 +- 選択が複数行の場合でも最後/全体矩形を基準に配置する。 +- viewport端ではポップオーバーが画面外へ出ないよう補正する。 +- scroll/resize時は再配置または非表示化する。 + +--- + +## 6. UI分離設計 + +### 6.1 Shadow DOM + +ページCSSとの衝突を避けるため、Content Scriptが専用host要素を追加し、その内部にShadow DOMを作る。 + +```js +const host = document.createElement('div'); +document.documentElement.append(host); +const shadow = host.attachShadow({ mode: 'open' }); +``` + +`open/closed` は実装都合で選択可能だが、セキュリティ境界として依存しない。 + +### 6.2 DOM挿入ルール + +禁止: +```js +element.innerHTML = translatedText; +shadowRoot.innerHTML = translatedText; +``` + +許可: +```js +element.textContent = translatedText; +``` + +静的テンプレートHTMLを自前固定文字列として構築する場合も、外部入力を文字列連結しない。 + +--- + +## 7. 翻訳ポップオーバー設計 + +### 7.1 状態 + +```text +HIDDEN +READY +LOADING +STREAMING +COMPLETED +ERROR +``` + +### 7.2 表示内容 + +| 要素 | 内容 | +|---|---| +| Header | 翻訳結果 | +| Meta | Model名 / Ollama | +| Body | streaming翻訳本文 | +| Copy | 完了/途中結果をclipboardへコピー | +| Regenerate | 同じ選択文字列で再要求 | +| Close | ポップオーバー非表示 | + +### 7.3 UI挙動 +- Icon押下直後にLOADING表示。 +- 最初のchunk受信後STREAMINGへ遷移。 +- 完了イベントでCOMPLETED。 +- Abortされた旧requestのchunkはrequestId不一致なら無視。 +- Error時は再生成可能な状態に戻す。 + +--- + +## 8. Runtime通信設計 + +Streamingには `browser.runtime.connect()` / `runtime.Port` を使用する。 + +### 8.1 Port名 + +```text +translation-stream +``` + +### 8.2 Content → Background message + +#### translate + +```js +{ + type: 'translate', + requestId: 'uuid-or-random-id', + text: 'selected text' +} +``` + +#### cancel + +```js +{ + type: 'cancel', + requestId: '...' +} +``` + +### 8.3 Background → Content message + +#### started + +```js +{ + type: 'started', + requestId: '...', + model: '...' +} +``` + +#### chunk + +```js +{ + type: 'chunk', + requestId: '...', + text: 'partial translation' +} +``` + +#### completed + +```js +{ + type: 'completed', + requestId: '...' +} +``` + +#### error + +```js +{ + type: 'error', + requestId: '...', + code: 'OLLAMA_UNAVAILABLE', + message: '...' +} +``` + +### 8.4 Validation +Backgroundは最低限以下を確認する。 +- `type` が既知値か +- `requestId` がstringか +- `text` がstringか +- text lengthが上限以内か +- 現在のsender/portがContent Script由来か + +--- + +## 9. TranslationSession設計 + +Backgroundではactive translationを原則1件だけ保持する。 + +```js +class TranslationSession { + requestId; + abortController; + sourceText; + startedAt; +} +``` + +新しい翻訳要求: + +```text +translate(request B) + ↓ +active request Aあり? + ├─ yes → Abort A + └─ no + ↓ +request Bをactiveに設定 + ↓ +Ollama streaming開始 +``` + +### 9.1 古いchunk対策 +受信chunkごとにactive requestIdを確認し、不一致なら破棄する。 + +--- + +## 10. OllamaClient設計 + +### 10.1 API + +Ollama REST APIをFetch APIで直接呼び出す。`ollama-js`等のサードパーティSDKは使用しない。 + +概念例: + +```js +const response = await fetch(`${host}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: settings.model, + messages: [ + { + role: 'system', + content: buildSystemPrompt(settings) + }, + { + role: 'user', + content: selectedText + } + ], + stream: true, + think: settings.thinking, // off時は省略 + options: buildOptions(settings) + }), + signal: abortController.signal +}); + +// NDJSONストリームをバッファリングしつつ行単位でパースする +const reader = response.body.getReader(); +const decoder = new TextDecoder(); +let buffer = ''; +for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + // bufferを行単位で分割し、完全な行のみJSON.parseする + // 末尾の不完全行は次回チャンクに持ち越す +} +``` + +### 10.2 System Prompt構築 + +```text +BUILTIN_SYSTEM_PROMPT + +[User translation preferences] +USER_SYSTEM_PROMPT +``` + +固定promptは常に先頭に置く。 + +### 10.3 buildOptions +未設定値はrequest objectへ含めない。 + +例: + +```js +{ + temperature: 0.2, + num_ctx: 16384, + seed: 42, + top_k: 40, + top_p: 0.9, + min_p: 0.05, + typical_p: 1.0, + repeat_last_n: 64, + repeat_penalty: 1.1, + presence_penalty: 0.0, + frequency_penalty: 0.0, + num_keep: 4, + num_predict: -1, + stop: [''] +} +``` + +Seed可変時は`seed`をoptionsへ含めない設計を優先する。 + +--- + +## 11. 設定スキーマ + +推奨形: + +```js +{ + enabled: true, + ollamaHost: 'http://localhost:11434', + model: '', + targetLanguage: 'ja', + thinking: false, + temperature: 0.2, + contextLength: 16384, + seedMode: 'random', + seedValue: null, + userSystemPrompt: '', + advanced: { + numKeep: null, + numPredict: null, + topK: null, + topP: null, + minP: null, + typicalP: null, + repeatLastN: null, + repeatPenalty: null, + presencePenalty: null, + frequencyPenalty: null, + stop: [] + } +} +``` + +--- + +## 12. 設定値validation + +### 12.1 Basic + +| enabled | boolean | +| ollamaHost | URL形式。**`http://localhost:` または `http://127.0.0.1:` のみ許可(schemeはhttp固定、外部ホスト・LAN IP・httpsは拒否)。必須制限** | +| model | non-empty string | +| targetLanguage | 定義済み値 | +| thinking | `false/true/low/medium/high` 相当 | +| temperature | 0.0〜2.0、0.1刻み | +| contextLength | `4096/8192/16384/32768/131072/262144` のいずれか | +| seedMode | `random/fixed` | +| seedValue | fixed時のみinteger | +| userSystemPrompt | string、最大長を設定 | + +### 12.2 Advanced + +推奨UI step: + +| 項目 | step | validation方針 | +|---|---:|---| +| num_keep | 1 | integer | +| num_predict | 1 | integer、-1許容 | +| top_k | 1 | integer、0以上 | +| top_p | 0.05 | float | +| min_p | 0.01 | float | +| typical_p | 0.05 | float | +| repeat_last_n | 1 | integer、-1許容 | +| repeat_penalty | 0.05 | float | +| presence_penalty | 0.05 | float | +| frequency_penalty | 0.05 | float | +| stop | - | string[] | + +Ollamaが明示的な最小/最大値を保証していない項目について、UI側で過度に狭い範囲を独自設定しない。型と明らかな不正値を中心に検証する。 + +--- + +## 13. Context Length UI設計 + +固定プリセット: + +```js +const CONTEXT_PRESETS = [ + 4096, + 8192, + 16384, + 32768, + 131072, + 262144 +]; +``` + +UI表示: + +```text +4k ─ 8k ─ 16k ─ 32k ─ 128k ─ 256k +``` + +実装上はrange inputのindex値を使用してよい。 + +```text +0 => 4096 +1 => 8192 +2 => 16384 +3 => 32768 +4 => 131072 +5 => 262144 +``` + +モデル自身が対応する最大contextを超える場合、Ollama側でエラーまたは制約される可能性があるため、そのエラーをUIへ返す。 + +--- + +## 14. Temperature UI設計 + +```text +min=0.0 +max=2.0 +step=0.1 +``` + +スライダー値は表示用数値と同期する。 + +```html + +``` + +--- + +## 15. Seed UI設計 + +```text +Seed +[ 可変 ] [ 固定 ] + +固定選択時: +固定値 (整数) [ 42 ▲▼ ] +``` + +- random時: seed optionを送らない。 +- fixed時: `seedValue` を整数として送る。 +- Seed Valueはnumber input + spinnerでよい。 + +--- + +## 16. Advanced UI設計 + +数値項目は`input type="number"`相当のUIとする。 + +例: + +```text +top_p +[ 0.90 ][▲] + [▼] +``` + +HTML native spinnerを使用できる場合は活用し、独自実装を増やさない。 + +stopは複数行textareaとし、1行1sequenceとして保存する。 + +--- + +## 17. SettingsService設計 + +### 17.1 保存 + +`browser.storage.local` を使用。 + +### 17.2 読み込み +1. default settings生成 +2. storage値取得 +3. merge +4. validation +5. 不正値はdefaultへフォールバック + +### 17.3 設定変更通知 +設定変更時に `browser.storage.onChanged` を使用し、Content Script / Background双方が必要な設定を更新できるようにする。 + +--- + +## 18. Security Design + +### 18.1 Trust Boundary + +```text +UNTRUSTED +Web Page / DOM / Selection + │ + ▼ +Content Script + │ structured messages only + ▼ +PRIVILEGED +Background / Ollama access / storage +``` + +### 18.2 XSS対策 +- 選択文字列をHTML化しない。 +- LLM出力をHTML化しない。 +- Markdown rendererを初期版では導入しない。 +- DOM挿入は`textContent`中心。 + +### 18.3 Prompt Injection対策 +翻訳用途ではページ本文を命令ではなくデータとして固定system promptで扱う。 + +ただしLLMの挙動を完全保証するセキュリティ境界としてPromptのみへ依存しない。 + +### 18.4 Remote Code禁止 +- CDN script禁止。 +- `eval`禁止。 +- `new Function`禁止。 +- remote import禁止。 +- npm依存はbundleに含める。 + +### 18.5 Storage +- `storage.local` は暗号化ストレージではない前提。 +- API key等の秘密情報は初期版では扱わない。 + +### 18.6 Message Validation +Content側のデータをBackgroundで再検証する。 + +### 18.7 Resource Abuse +- MAX_SELECTION_LENGTHを設ける。 +- active translationは1件。 +- 新規requestで旧request abort。 +- 選択だけではrequestを送らない。 + +--- + +## 19. Error Code設計 + +| Code | 意味 | +|---|---| +| `INVALID_REQUEST` | message形式不正 | +| `TEXT_EMPTY` | 空文字 | +| `TEXT_TOO_LONG` | 選択長超過 | +| `OLLAMA_UNAVAILABLE` | Ollama接続不可 | +| `MODEL_NOT_FOUND` | モデルなし | +| `INVALID_SETTINGS` | 設定値不正 | +| `REQUEST_ABORTED` | 新規要求等で中断 | +| `STREAM_ERROR` | streaming中エラー | +| `UNKNOWN_ERROR` | その他 | + +UIでは内部stack traceを表示しない。 + +--- + +## 20. Copy設計 + +Clipboard APIのみを使用する。`document.execCommand('copy')` はdeprecatedのため使用しない。 + +- コピーはポップオーバーのコピーボタンclick(user activation内)で発生するため、`navigator.clipboard.writeText()` をそのハンドラ内で直接呼び出す。 +- 書き込み失敗時は「コピーできませんでした」等の簡潔なメッセージをポップオーバーへ表示するのみとし、deprecated APIへフォールバックしない。 +- Firefox実機でclipboard動作を確認する。 + +コピー対象は現在表示中の翻訳プレーンテキストのみ。 + +--- + +## 21. モデル一覧取得 + +BackgroundのOllamaClientからモデル一覧取得APIを呼び、Settings UIへ返す。 + +要件: +- Ollama未起動時は空一覧 + 接続エラー表示。 +- 取得成功時はselect候補へ反映。 +- 保存済みmodelが一覧にない場合は警告表示。 + +--- + +## 22. 実装順序 + +### Phase 1: Skeleton +- Manifest V3 +- Background +- Content Script +- Popup / Options +- build/test基盤 + +### Phase 2: Selection UI +- Selection検出 +- Action button +- Shadow DOM +- Popover skeleton + +### Phase 3: Messaging +- runtime.Port +- message schema +- validation +- requestId + +### Phase 4: Ollama +- Ollama REST client(fetch + NDJSON streaming) +- model list +- chat stream +- Abort + +### Phase 5: Settings +- storage.local +- Basic settings +- Advanced settings +- Context slider +- Temperature slider +- Seed modes + +### Phase 6: Security Hardening +- textContent確認 +- CSP確認 +- message validation +- input length limits +- no remote code +- permissions audit + +### Phase 7: Tests +- unit +- integration +- Firefox実機 + +### Phase 8: Final Verification +- lint/test/build +- temporary add-on install +- acceptance criteriaを全件確認 + +--- + +## 23. テスト設計 + +### 23.1 Unit + +| 対象 | ケース | +|---|---| +| settings schema | default / valid / invalid | +| temperature | 0.1刻み / 範囲外 | +| contextLength | presetのみ許可 | +| seed | random / fixed / invalid integer | +| advanced options | null除外、数値反映 | +| prompt builder | fixed + user prompt結合 | +| message validation | malformed message拒否 | +| length limit | 境界値 | + +### 23.2 Integration + +Mock Ollama serverまたはmock clientを使い以下を確認。 + +```text +Content request +→ Background validation +→ Ollama stream +→ chunk forwarding +→ complete +``` + +Abortテスト: + +```text +request A start +request B start +A abort +B chunks only displayed +``` + +### 23.3 Firefox Manual/E2E + +対象ページ: +- 単純HTML +- 長文記事 +- 選択文字列にHTMLタグ表現を含むページ +- CSSが強く指定されたページ +- スクロール位置が深いページ + +確認: +- 選択位置近傍UI +- viewport端補正 +- scroll時挙動 +- selection解除 +- copy +- regenerate +- close +- settings persistence +- Ollama down/up + +--- + +## 24. Firefox API監査チェックリスト + +実装完了時に以下を確認する。 + +- [ ] Manifest V3 +- [ ] `action` 使用 +- [ ] `background.scripts` 使用 +- [ ] `background.service_worker` 不使用 +- [ ] `browser_action` 不使用 +- [ ] `tabs.executeScript()` 不使用 +- [ ] `runtime.getBackgroundPage()` 不使用 +- [ ] `window.localStorage` 不使用 +- [ ] `storage.local` 使用 +- [ ] `runtime.Port` streaming通信 +- [ ] Content ScriptからOllama fetchなし +- [ ] `eval` / `new Function`なし +- [ ] Remote JSなし +- [ ] LLM出力への`innerHTML`なし +- [ ] 不要permissionsなし +- [ ] iframe初期対応なし + +--- + +## 25. 完了時にLLMへ要求する成果物 + +実装担当LLMは最終的に以下を提示すること。 + +1. 実装済みソースコード一式 +2. `manifest.json` +3. build手順 +4. Firefox一時アドオンとしてのロード手順 +5. Ollama起動/モデル準備手順 +6. 単体テスト結果 +7. 結合テスト結果 +8. Firefox実機動作確認結果 +9. 使用WebExtensions API一覧 +10. 要求permissions一覧と理由 +11. セキュリティチェック結果 +12. 未解決事項・既知制限 + +テストを実行せず「実装したので動くはず」として完了扱いにしてはならない。実際に実行可能なテストは実行し、Firefox実機確認項目も可能な範囲で実行結果を記録する。 diff --git a/doc/Security.md b/doc/Security.md new file mode 100644 index 0000000..956f169 --- /dev/null +++ b/doc/Security.md @@ -0,0 +1,804 @@ +# Security.md + +## 1. Purpose + +This document defines the mandatory security constraints and implementation rules for the Firefox WebExtension project. + +The implementation agent MUST treat this file as a normative development policy together with `Specifications.md` and `Design.md`. + +When there is a conflict between convenience and this security policy, this policy takes precedence unless the project owner explicitly changes the requirement. + +--- + +## 2. Security Objectives + +| ID | Objective | +|---|---| +| SEC-001 | Minimize the extension's attack surface. | +| SEC-002 | Avoid third-party build-time and runtime dependency supply-chain risk. | +| SEC-003 | Treat all web page content as untrusted input. | +| SEC-004 | Prevent selected text and LLM output from being evaluated as HTML or JavaScript. | +| SEC-005 | Restrict privileged WebExtension operations to the minimum required components. | +| SEC-006 | Restrict network communication to the configured Ollama endpoint. | +| SEC-007 | Use current Firefox WebExtensions APIs and avoid deprecated or obsolete APIs. | +| SEC-008 | Keep the implementation auditable without a package manager or generated dependency tree. | + +--- + +## 3. Dependency Policy + +### 3.1 Mandatory Rules + +The project MUST be developed without Node.js or JavaScript package managers. + +The following are prohibited: + +- Node.js as a required development or runtime dependency +- npm +- yarn +- pnpm +- bun package management +- `node_modules` +- third-party JavaScript packages +- third-party CSS frameworks +- remote JavaScript +- remote CSS +- remote fonts +- CDN-hosted runtime resources + +The project SHOULD NOT contain `package.json`. + +All resources required at runtime MUST be included directly in the extension package or supplied by Firefox/Web Platform standard APIs. + +### 3.2 Allowed Technologies + +The implementation MAY use: + +- HTML +- locally stored CSS +- vanilla JavaScript +- Firefox WebExtensions APIs +- standard DOM APIs +- Fetch API +- Streams API +- `TextDecoder` / `TextDecoderStream` +- standard `JSON` APIs +- locally bundled PNG/SVG assets +- inline SVG created by the project +- operating-system fonts through CSS generic/system font families + +### 3.3 CSS and Asset Policy + +All application CSS MUST be owned by this project and stored locally in the repository. + +Do not download and vendor a CSS framework unless explicitly approved. + +Preferred approach: + +```text +content.css +popup.css +options.css +``` + +External resources such as the following MUST NOT be used: + +```text +Google Fonts +Font Awesome CDN +Material Icons CDN +Bootstrap CDN +Tailwind CDN +jsDelivr +unpkg +cdnjs +``` + +Icons SHOULD be: + +1. project-owned local SVG/PNG files, or +2. inline SVG defined directly by the application. + +System fonts SHOULD be used, for example: + +```css +font-family: system-ui, sans-serif; +``` + +--- + +## 4. Ollama Integration Policy + +### 4.1 No Ollama JavaScript SDK Dependency + +The project MUST NOT require `ollama-js`. + +The extension MUST communicate with Ollama directly using the official Ollama HTTP API and the browser Fetch API. + +The intent is to eliminate npm/package-manager supply-chain dependencies. + +### 4.2 Required API Scope + +The initial implementation SHOULD require only the minimum Ollama endpoints needed by the specification. + +Expected endpoints: + +| Endpoint | Purpose | +|---|---| +| `GET /api/tags` | Retrieve installed models. | +| `POST /api/chat` | Perform translation generation using streaming. | + +Do not add Ollama API endpoints unless required by a defined feature. + +### 4.3 Streaming + +Translation MUST use streaming responses. + +The Ollama client module MUST: + +- parse streamed response data safely; +- tolerate chunk boundaries that do not align with JSON object boundaries; +- cancel the current request when a newer translation request supersedes it; +- use `AbortController` or equivalent standard browser functionality; +- propagate only validated translation text to the content script. + +### 4.4 Ollama Endpoint + +Default endpoint: + +```text +http://localhost:11434 +``` + +The endpoint MAY be configurable according to the product specification. In the initial version, the configured endpoint MUST be limited to `http://localhost:` or `http://127.0.0.1:`. External hosts, LAN IPs, and HTTPS endpoints MUST be rejected by validation. + +The implementation MUST validate the configured value before use, and the Ollama client MUST re-verify the target host immediately before each request (defense in depth). + +The background component MUST be the only component that communicates directly with Ollama. + +Content scripts MUST NOT directly call Ollama. + +--- + +## 5. Firefox WebExtensions Policy + +### 5.1 Manifest + +The extension MUST use Manifest V3. + +Firefox-specific behavior MUST be verified against current MDN documentation before implementation. + +Do not blindly copy Chromium-specific Manifest V3 examples. + +### 5.2 Background Implementation + +For Firefox, use the supported background mechanism documented for Firefox. + +Do NOT assume Chromium's: + +```json +"background": { + "service_worker": "background.js" +} +``` + +is supported by Firefox. + +The implementation agent MUST verify the current Firefox support status before changing the background model. + +### 5.3 APIs to Prefer + +Use modern APIs where required: + +| Purpose | Preferred API | +|---|---| +| Toolbar action | `action` | +| Extension storage | `storage.local` | +| Long-lived extension communication | `runtime.connect()` / `runtime.Port` | +| One-shot extension messages | `runtime.sendMessage()` | +| Dynamic script injection, only if needed | `scripting.executeScript()` | + +### 5.4 APIs / Patterns to Avoid + +Do not introduce the following unless explicit project requirements justify them: + +- Manifest V2 +- `browser_action` +- `browserAction.*` +- `tabs.executeScript()` +- `tabs.insertCSS()` +- `runtime.getBackgroundPage()` +- `extension.getBackgroundPage()` +- `window.localStorage` for extension settings +- `externally_connectable` +- `runtime.onMessageExternal` +- `runtime.onConnectExternal` +- `nativeMessaging` +- `webRequest` +- `cookies` +- `history` +- `bookmarks` +- `downloads` +- unnecessary `tabs` permission +- unnecessary `scripting` permission + +The implementation MUST verify whether any WebExtensions API it intends to add is deprecated, obsolete, Firefox-incompatible, or unnecessarily privileged. + +--- + +## 6. Permission Minimization + +The extension MUST request only permissions required by implemented features. + +Expected initial permission set: + +```text +storage +``` + +Expected host permission: + +```text +http://localhost/* +http://127.0.0.1/* +``` + +Firefox match patterns do not support port numbers (Firefox bug 1362809 / 1468162). Therefore the patterns above match any port on localhost. The application MUST restrict the actual connection target to `http://localhost:` or `http://127.0.0.1:` via its own host validation before any Ollama request. + +or the minimum equivalent required by the final Ollama-host design. + +Page access MAY require content-script match patterns for HTTP/HTTPS pages because translation is initiated by selecting page text. + +Do not add broad privileged permissions for convenience. + +Every newly introduced permission MUST be justified in code review. + +--- + +## 7. Trust Boundaries + +The system has three primary trust zones. + +```text +Untrusted Web Page + | + | selected text + v +Content Script + | + | validated extension messages + v +Background + | + | HTTP + v +Ollama +``` + +### 7.1 Web Page + +All page content MUST be considered untrusted. + +This includes: + +- text selected by the user; +- DOM content; +- attributes; +- page-generated text; +- scripts; +- event data; +- iframe content; +- strings that resemble prompts or instructions. + +The presence of text such as: + +```text +Ignore previous instructions... +``` + +MUST NOT cause that text to be treated as an application instruction. + +Selected web content is translation data, not trusted control input. + +### 7.2 Content Script + +The content script is responsible only for tasks that require page access, including: + +- obtaining the user's current text selection; +- positioning the translation action icon; +- rendering the translation popover; +- forwarding translation requests; +- receiving streamed translation text; +- handling local UI interaction. + +It MUST NOT be responsible for privileged network communication with Ollama. + +### 7.3 Background + +The background component owns privileged application operations: + +- Ollama HTTP communication; +- request lifecycle management; +- configuration access where appropriate; +- message validation; +- request cancellation; +- Ollama response parsing. + +--- + +## 8. DOM and XSS Rules + +### 8.1 Plain-Text Rendering + +Page-derived text and LLM-generated text MUST be rendered as plain text. + +Use safe DOM APIs such as: + +```js +element.textContent = value; +``` + +Do NOT use untrusted content with: + +```js +element.innerHTML = value; +element.outerHTML = value; +shadowRoot.innerHTML = value; +document.write(value); +``` + +### 8.2 No HTML Interpretation + +The translation result is plain text. + +Do not implement Markdown rendering, HTML rendering, or rich-text parsing unless explicitly added as a later requirement and separately security-reviewed. + +### 8.3 No Dynamic Code Execution + +The following are prohibited: + +```text +eval() +new Function() +setTimeout(string) +setInterval(string) +``` + +No selected page content, setting, LLM response, or remote response may be executed as JavaScript. + +--- + +## 9. Shadow DOM Policy + +The translation UI SHOULD use Shadow DOM where practical to isolate extension UI styling from page CSS. + +Primary reasons: + +- reduce CSS collisions; +- reduce page-style interference; +- contain translation UI styles; +- improve predictable layout. + +Shadow DOM MUST NOT be treated as an XSS security boundary. + +LLM responses and page text inside a shadow root MUST still use plain-text rendering. + +--- + +## 10. Page Script Isolation + +The extension SHOULD rely on the normal WebExtensions content-script isolation boundary. + +Do not bridge page scripts and extension scripts unless a future requirement makes this unavoidable. + +The following SHOULD NOT be used: + +- `wrappedJSObject` +- `exportFunction` +- `cloneInto` +- custom page-script injection +- page-controlled `window.postMessage` communication + +If page-script communication becomes necessary later, it requires an explicit security design update. + +--- + +## 11. Message Validation + +All messages received by privileged extension components MUST be validated. + +Example translation request shape: + +```js +{ + type: "translate", + requestId: "...", + text: "..." +} +``` + +The receiver MUST validate at least: + +- message is an object; +- known `type`; +- expected fields are present; +- `text` is a string; +- `requestId` is a string; +- text length is within defined limits; +- unsupported fields do not alter privileged behavior. + +Do not dispatch privileged behavior using arbitrary method names supplied by messages. + +Avoid patterns such as: + +```js +handlers[message.method](message); +``` + +unless `message.method` is strictly allow-listed. + +--- + +## 12. Input Limits and Resource Abuse + +Selected text MUST have a maximum allowed size. + +The exact product limit MAY be defined in `Specifications.md`, but an implementation limit MUST exist. + +The purpose is to reduce: + +- accidental huge requests; +- malicious resource consumption; +- unnecessarily long model generation; +- UI hangs; +- memory pressure. + +If input exceeds the allowed limit, the extension MUST fail safely and provide a user-visible error. + +Repeated translation requests MUST cancel obsolete in-flight requests. + +--- + +## 13. LLM Prompt Boundary + +The application MUST maintain a separation between: + +1. application-controlled system instructions; +2. user-configurable system instructions; +3. selected page text. + +The application-controlled prompt MUST state that selected page content is translation material and must not be interpreted as instructions. + +Selected text MUST NOT be concatenated into trusted system instructions. + +Preferred conceptual request structure: + +```text +system: + application-controlled translation rules + + validated user translation preferences + +user: + selected page text +``` + +--- + +## 14. Settings Validation + +Every configurable Ollama generation value MUST be validated before being sent to Ollama. + +Validation MUST include: + +- expected type; +- finite numeric value where applicable; +- supported range; +- integer-only constraints where applicable; +- allowed enum values; +- context-length preset membership; +- string length limits. + +Invalid stored values MUST NOT be passed directly to Ollama. + +The implementation SHOULD fall back to safe defaults or request correction from the user. + +--- + +## 15. Seed Security / Correctness Rule + +Seed configuration MUST distinguish between: + +- variable/random behavior; +- fixed deterministic integer seed. + +When fixed seed mode is enabled, the value MUST be validated as an integer. + +When variable mode is enabled, the implementation MUST use the Ollama/API-supported behavior defined in the specification and MUST NOT generate executable or unsafe random data. + +--- + +## 16. Storage Rules + +Use: + +```text +browser.storage.local +``` + +for extension configuration. + +Do not use page `localStorage` for extension settings. + +The implementation MUST assume `storage.local` is not encrypted. + +Do not store: + +- passwords; +- arbitrary secrets; +- private authentication tokens; + +unless a later requirement explicitly adds such functionality and the security model is revised. + +The initial local-Ollama design SHOULD require no secret credentials. + +--- + +## 17. Content Security Policy + +Keep Firefox's extension CSP as restrictive as practical. + +Do not weaken CSP merely to make a library or framework work. + +Do not enable: + +- remote script execution; +- `unsafe-eval`; +- arbitrary inline executable JavaScript. + +JavaScript SHOULD be stored in local `.js` files where applicable. + +HTML event attributes such as the following SHOULD NOT be used: + +```html +