# Agent handshake — Firefox Agent Bridge Use this when a script or coding agent needs to **create, update, move, or delete Firefox bookmarks while Firefox is open**. Do not edit `places.sqlite` if this bridge is reachable. ## Preconditions 1. Native host installed: `powershell -NoProfile -File tools/install-native-host.ps1` 2. A **registered client key** for this tool (see Auth). The host has only the public half. 3. Extension loaded in the target Firefox profile (`about:debugging` temporary add-on, or a signed install) 4. Firefox **running** (the host process is started by the extension) If `http://127.0.0.1:17634/health` fails, the host is not up — open Firefox and confirm the extension is loaded. If `/health` works but `/v1/ready` fails, the stdio pipe is down; reload the extension. ## Auth The loopback port is **not** an open local API. Each caller is a named Ed25519 client. Register once per tool (human or agent operator does this; do not write the private key under `%LOCALAPPDATA%\firefox-agent-bridge`): ```text python tools/register_client.py add --name cursor-agent --write-key %USERPROFILE%\.fab\cursor-agent.json ``` Give tooling **only** that key file: ```text set FAB_KEY_FILE=%USERPROFILE%\.fab\cursor-agent.json ``` or `python tools/client.py --key PATH …`. The host stores public keys in `%LOCALAPPDATA%\firefox-agent-bridge\clients.json`. Revoke with `python tools/register_client.py revoke NAME_OR_ID`. List with `… list`. Every request except `/health` must be signed: ``` Authorization: FAB-ED25519 id= X-FAB-Timestamp: X-FAB-Nonce: X-FAB-Signature: ``` Canonical message (UTF-8, newline-separated): ```text v1 ``` Skew allowance is 90 seconds. Nonces cannot be reused. `tools/client.py` builds this for you. Do not use a shared bearer token. Do not commit private key bundles. Do not paste PEM material into chat. ## Preferred caller ```text python tools/client.py --key KEYFILE METHOD [ARGS_JSON] ``` `ARGS_JSON` is a JSON **array** matching the WebExtension function arguments. Examples: ```text python tools/client.py --key %USERPROFILE%\.fab\cursor-agent.json meta.methods python tools/client.py --key %USERPROFILE%\.fab\cursor-agent.json bookmarks.search "[{\"title\":\"10.132.x.x\"}]" ``` HTTP equivalent: `POST /v1/call` with the signature headers above and body `{"method":"bookmarks.search","args":[{"title":"10.132.x.x"}]}`. ## Allowlisted methods (v0.1) | Method | Args | Notes | |--------|------|--------| | `meta.ping` | (none) | Bridge alive | | `meta.methods` | (none) | This table | | `bookmarks.getTree` | (none) | Full tree | | `bookmarks.getSubTree` | `id` | | | `bookmarks.getChildren` | `id` | | | `bookmarks.get` | `id` or `[id, …]` | | | `bookmarks.search` | `{title? url? query?}` or string | | | `bookmarks.create` | `{parentId?, title, url?}` | Omit `url` to create a folder | | `bookmarks.update` | `id`, `{title? url?}` | | | `bookmarks.move` | `id`, `{parentId? index?}` | | | `bookmarks.remove` | `id` | Bookmark or empty folder | | `bookmarks.removeTree` | `id` | Folder and descendants | | `bookmarks.getRecent` | `numberOfItems` | | Do not invent other `browser.*` names. Adding an API means editing `ALLOWED` in `extension/background.js` and reloading the extension. REST aliases (same signature): | HTTP | Maps to | |------|---------| | `GET /health` | host process only (no key) | | `GET /v1/ready` | `meta.ping` | | `GET /v1/methods` | `meta.methods` | | `GET /v1/bookmarks/tree` | `bookmarks.getTree` | | `GET /v1/bookmarks/{id}` | `bookmarks.get` | | `POST /v1/bookmarks/search` | body is the query object | | `POST /v1/bookmarks/create` | body is `CreateDetails` | | `POST /v1/bookmarks/update` | `{id, changes}` | | `POST /v1/bookmarks/move` | `{id, destination}` | | `POST /v1/bookmarks/remove` | `{id}` | | `POST /v1/bookmarks/remove-tree` | `{id}` | | `POST /v1/call` | `{method, args}` | ## Typical folder replace 1. `bookmarks.search` `{title: "…"}` — take the hit **without** a `url` (that is the folder). Fail if zero or many. 2. `bookmarks.getChildren` on that id. 3. `bookmarks.remove` each bookmark; `bookmarks.removeTree` each child folder. 4. `bookmarks.create` each new item with `parentId` set. `examples/replace_named_folder.py` does exactly that (`FAB_KEY_FILE` must be set). It is an example, not a sync service. You cannot modify Firefox's bookmark root (`The bookmark root cannot be modified`). Operate on a named subfolder (toolbar / menu / a folder the user already created). ## Failure modes | Symptom | Cause | What to do | |---------|--------|------------| | Connection refused on `:17634` | Firefox closed or extension not loaded | Open Firefox; load/reload the add-on | | 401 `signed FAB-ED25519 client required` | Old bearer token or unsigned curl | Use `tools/client.py` and a registered key | | 401 `unknown client id` | Key revoked or host has no `clients.json` | `register_client.py list` / `add` | | 401 `bad signature` / `replayed nonce` / skew | Wrong key, reused request, or clock drift | New request; check `FAB_KEY_FILE` | | 502 `method not allowed` | Typo or API not in `ALLOWED` | Use `meta.methods` | | 502 timeout | Extension died mid-call | Reload the add-on | | Temporary add-on gone after restart | Unsigned on Firefox Release | Load again, or sign via AMO unlisted | | Native host not found | Installer not run | `tools/install-native-host.ps1` | ## Security boundaries - The extension does **not** listen on `runtime.onMessageExternal`. Other add-ons cannot call the dispatcher. - The native host manifest `allowed_extensions` is pinned to `firefox-agent-bridge@easygoingaming.com`. A different add-on cannot `connectNative` to this host. - HTTP is `127.0.0.1` only. Requests that carry a browser `Origin` header are rejected. `Host` must be loopback. - Local processes **without** a registered private key cannot edit bookmarks through this port. The public store is useless for impersonation. - Another add-on that already has the `bookmarks` permission does not need this bridge. - Do not add `tabs`, `history`, ``, or `onMessageExternal` without a new threat review. ## What this project is not - Not a bidirectional sync implementation - Not a general Firefox remote-control surface (no tabs, history, cookies, native file access) - Not a reason to keep editing `places.sqlite` on a live profile