Files
bookmarks-api/AGENTS.md
T

6.4 KiB

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):

python tools/register_client.py add --name cursor-agent --write-key %USERPROFILE%\.fab\cursor-agent.json

Give tooling only that key file:

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=<client_id>
X-FAB-Timestamp: <unix seconds>
X-FAB-Nonce: <unique hex>
X-FAB-Signature: <base64 Ed25519 of canonical message>

Canonical message (UTF-8, newline-separated):

v1
<client_id>
<timestamp>
<nonce>
<HTTP_METHOD>
<path>
<sha256 hex of raw body>

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

python tools/client.py --key KEYFILE METHOD [ARGS_JSON]

ARGS_JSON is a JSON array matching the WebExtension function arguments.

Examples:

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, <all_urls>, 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