Present the repo as a scoped bookmarks API product and drop homelab-only notes.

This commit is contained in:
alexveley
2026-09-22 08:10:02 -04:00
parent c19e619b2f
commit cfda629418
10 changed files with 123 additions and 45 deletions
+8 -8
View File
@@ -12,15 +12,15 @@ Do not edit `places.sqlite` while Firefox is open if this API is reachable.
2. Native host installed only if scripts should call in: `powershell -NoProfile -File tools/install-native-host.ps1`
3. Firefox **running** for those calls
If `http://127.0.0.1:17634/health` fails, the host is not up. If `/health` works but calls 401, you do not have a current `FAB_TOKEN`.
If `http://127.0.0.1:17634/health` fails, the host is not up. If `/health` works but calls 401, you do not have a current token.
## Auth
This is a normal API token, not an SSH key.
1. In Firefox, open the Bookmarks API toolbar icon → **Manage clients**.
2. Name the client (e.g. `cursor-agent`) and click **Generate secret**.
3. Copy the `fab_…` value **once**. Store it however you already store secrets: env var, Vaultwarden, `.env`, CI secret, Cursor env.
2. Name the client and click **Generate secret**.
3. Copy the `fab_…` value **once**. Store it with your other secrets (environment variable, password manager, `.env`, or CI secret).
4. Give tooling only that string:
```text
@@ -41,7 +41,7 @@ python tools/client.py METHOD [ARGS_JSON]
`ARGS_JSON` is a JSON **array** matching the WebExtension function arguments.
```text
python tools/client.py bookmarks.search "[{\"title\":\"10.132.x.x\"}]"
python tools/client.py bookmarks.search "[{\"title\":\"Research\"}]"
```
HTTP:
@@ -51,10 +51,10 @@ POST /v1/call
Authorization: Bearer fab_
Content-Type: application/json
{"method": "bookmarks.search", "args": [{"title": "10.132.x.x"}]}
{"method": "bookmarks.search", "args": [{"title": "Research"}]}
```
## Allowlisted methods (v0.2)
## Allowlisted methods
| Method | Args | Notes |
|--------|------|--------|
@@ -72,7 +72,7 @@ Content-Type: application/json
| `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.
Do not invent other `browser.*` names. Adding an API means editing `ALLOWED` in `extension/background.js` **and** `ALLOWED_METHODS` in `host/firefox_agent_bridge_host.py`, then reloading the extension.
REST aliases use the same Bearer token. `GET /health` has no token (loopback liveness only).
@@ -92,7 +92,7 @@ REST aliases use the same Bearer token. `GET /health` has no token (loopback liv
| Connection refused on `:17634` | Firefox closed or add-on/host missing | Open Firefox; load add-on; install native host |
| 401 `Bearer token required` | Unsigned request | Set `FAB_TOKEN` |
| 401 `unknown token` | Revoked, typo, or never generated | Manage clients → generate again |
| 502 `method not allowed` | Typo or API not in `ALLOWED` | `meta.methods` |
| 502 `method not allowed` | Typo or API not in the allowlist | `meta.methods` |
| Temporary add-on gone after restart | Unsigned on Firefox Release | Load again, or sign via AMO |
## Security boundaries
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 EasyGoin / EGG
Copyright (c) 2026 EasyGoin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+23
View File
@@ -0,0 +1,23 @@
# Privacy
Bookmarks API for Scripting and AI runs on your computer. It does not send your bookmarks, secrets, or browsing data to the authors or to any remote service.
## What it can see
With the `bookmarks` permission, the add-on can read and change bookmarks in the Firefox profile where it is installed. That is the product. Local scripts you authorize can request the same operations through the loopback API.
## What it stores
Manage clients stores **SHA-256 hashes** of issued secrets, plus the client name and timestamps, in `browser.storage.local`. The plaintext secret is shown once and is not kept.
The add-on does not use `storage.sync`, so those hashes are not uploaded with a Firefox Account.
## What it does not collect
No telemetry, crash reports, or analytics. No account. No network calls to the authors.
The optional native host listens on `127.0.0.1` only so *your* local tools can reach the add-on. It does not accept browser-origin requests.
## Revoking access
Toolbar → **Manage clients****Revoke**. After that, the old secret is rejected.
+46 -12
View File
@@ -1,25 +1,59 @@
# Bookmarks API for Scripting and AI
An API add-on for Firefox to allow agentic and script-based management of user bookmarks.
An API add-on for Firefox so agents and scripts can manage your bookmarks while the browser is open.
The published product is the add-on in `extension/`. Issue a secret in **Manage clients**, store it with your secrets, and keep Firefox open so those tools can talk to this add-on.
The shipped product is the add-on in `extension/`. This is not a bookmark sync engine, and it is not a Mozilla product.
This is not a bookmark sync engine, and it is not a Mozilla product.
## What it does
## Setup
Scripts and local agents call a loopback HTTP API. The add-on performs only `browser.bookmarks` operations (create, search, move, delete, and related reads). It does not read tabs, history, cookies, or the open web.
1. Load the add-on (temporary via `about:debugging`, or a signed `.xpi` — [docs/signing.md](docs/signing.md)).
2. Toolbar → **Manage clients** → generate a secret → store it with your secrets.
3. To let scripts call in, register the native host once (`tools/install-native-host.ps1` on this workstation).
1. Load the add-on.
2. Toolbar → **Manage clients** → generate a secret → store that secret yourself.
3. Register the native host once if scripts should call in (`tools/install-native-host.ps1` on Windows).
4. Keep Firefox open. Call `http://127.0.0.1:17634` with `Authorization: Bearer <secret>`.
Authenticated calls use `Authorization: Bearer <secret>` on `http://127.0.0.1:17634`. Method list for people working in this repo: [AGENTS.md](AGENTS.md).
```http
POST /v1/call
Authorization: Bearer fab_
Content-Type: application/json
{"method": "bookmarks.search", "args": [{"title": "Research"}]}
```
```text
set FAB_TOKEN=fab_…
python tools/client.py meta.methods
python tools/client.py bookmarks.search "[{\"title\":\"Research\"}]"
```
Method list and failure modes: [AGENTS.md](AGENTS.md). Signing for Firefox Release: [docs/signing.md](docs/signing.md).
## Permissions
| Permission | Why |
|------------|-----|
| `bookmarks` | The only WebExtension API this add-on calls |
| `storage` | SHA-256 hashes of issued client secrets |
| `nativeMessaging` | The only inbound path from local scripts (Mozilla does not allow a raw listening socket in the extension) |
It does **not** request `tabs`, `history`, `cookies`, `<all_urls>`, `webRequest`, or `runtime.onMessageExternal`.
## Security
- Binds **127.0.0.1** only. Browser `Origin` headers are rejected.
- Client hashes live in the add-ons storage. The host asks the add-on whether a token is valid.
- Only `bookmarks.*` (plus `meta.*`). No history, cookies, tabs, or `onMessageExternal`.
- HTTP binds **127.0.0.1** only. Requests with a browser `Origin` header are rejected.
- The native host may talk only to this add-ons gecko id.
- The add-on stores **hashes**, not secrets. Revoke from Manage clients.
- Both the host and the add-on allowlist the same `meta.*` / `bookmarks.*` methods.
See [PRIVACY.md](PRIVACY.md) and [SECURITY.md](SECURITY.md).
## What this is not
- Not bidirectional bookmark sync
- Not a general Firefox remote-control surface
- Not a reason to edit `places.sqlite` while Firefox is running
## License
MIT.
MIT. Copyright EasyGoin.
+16
View File
@@ -0,0 +1,16 @@
# Security
This add-on is scoped to bookmarks. Treat a leaked `fab_…` secret like a password for your bookmark tree: revoke it, then issue a new one.
## Boundaries that must stay
- Manifest permissions: `bookmarks`, `storage`, `nativeMessaging` only
- No `tabs`, `history`, `cookies`, `<all_urls>`, `webRequest`, or `runtime.onMessageExternal`
- Host bind: `127.0.0.1`
- Reject HTTP requests that carry a browser `Origin`
- Native host `allowed_extensions` pinned to this gecko id
- Method allowlist in both `extension/background.js` and `host/firefox_agent_bridge_host.py`
## Reporting
Please open an issue on this repository. Do not attach live secrets or a copy of `places.sqlite`.
+7 -7
View File
@@ -1,6 +1,6 @@
# Signing for Firefox Release
Firefox Release and Beta will only keep an add-on that Mozilla has signed. Temporary add-ons from `about:debugging` die on restart. Developer Edition / Nightly can load unsigned builds if `xpinstall.signatures.required` is false — do not rely on that for this workstation.
Firefox Release and Beta will only keep an add-on that Mozilla has signed. Temporary add-ons from `about:debugging` are removed on restart. Developer Edition / Nightly can load unsigned builds if `xpinstall.signatures.required` is false — do not rely on that for daily use.
Mozilla signs through [addons.mozilla.org](https://addons.mozilla.org/) even when the add-on is **not** listed in the store.
@@ -9,14 +9,14 @@ Mozilla signs through [addons.mozilla.org](https://addons.mozilla.org/) even whe
1. Register at [addons.mozilla.org](https://addons.mozilla.org/) (Mozilla account).
2. Open [API credentials](https://addons.mozilla.org/developers/addon/api/key/).
3. Generate a JWT **issuer** (`user:…`) and **secret**.
4. Store both in Vaultwarden. Never commit them. Env names `WEB_EXT_API_KEY` / `WEB_EXT_API_SECRET` (or `AMO_JWT_ISSUER` / `AMO_JWT_SECRET` if you prefer wrappers).
4. Store both in your password manager. Never commit them. Env names `WEB_EXT_API_KEY` / `WEB_EXT_API_SECRET`.
## 2. Choose a channel
| Channel | What you get |
|---------|----------------|
| **unlisted** | Signed `.xpi` for self-install. Not on the AMO store. Enough to survive Firefox restarts. **Start here.** |
| **listed** | Public AMO listing, automatic updates via Firefox. Needs listing metadata, review, and Add-on Policy compliance. Do this when you want strangers to find it. |
| **listed** | Public AMO listing, automatic updates via Firefox. Needs listing metadata, review, and Add-on Policy compliance. |
Unlisted submissions can still be pulled for manual review. Native messaging is allowed; be ready to explain that the HTTP listener is the **native host**, bind is loopback-only, tokens are hashed at rest, and the extension does not accept `onMessageExternal`.
@@ -30,16 +30,16 @@ cd extension
web-ext sign --channel=unlisted --api-key $env:WEB_EXT_API_KEY --api-secret $env:WEB_EXT_API_SECRET
```
The gecko id in `manifest.json` (`firefox-agent-bridge@easygoingaming.com`) must stay stable. Bump `version` for every new sign.
The gecko id in `manifest.json` must stay stable. Bump `version` for every new sign.
`web-ext` writes a signed `.xpi` under `web-ext-artifacts/`. Install it in Firefox: the `.xpi` file, or `about:addons` → gear → Install Add-on From File.
`web-ext` writes a signed `.xpi` under `web-ext-artifacts/`. Install it in Firefox from that file, or `about:addons` → gear → Install Add-on From File.
Keep the native host registered (`tools/install-native-host.ps1`). Signing replaces only the extension; the host is unchanged.
## 4. Updates
Bump `version`, sign again on the same channel and id. For unlisted self-distribution, [updates](https://extensionworkshop.com/documentation/manage/updating-your-extension/) need an `update_url` in `browser_specific_settings.gecko` if you want Firefox to auto-fetch. Until that exists, drop in a new `.xpi` by hand.
Bump `version`, sign again on the same channel and id. For unlisted self-distribution, [updates](https://extensionworkshop.com/documentation/manage/updating-your-extension/) need an `update_url` in `browser_specific_settings.gecko` if you want Firefox to auto-fetch. Until that exists, install a new `.xpi` by hand.
## 5. Listed (later)
`web-ext sign --channel=listed` plus an AMO metadata JSON (name **Bookmarks API for Scripting and AI**, license MIT). Listing copy: local scripts only, loopback HTTP, bearer token, bookmarks allowlist. Do not title it “Firefox …”. Mozilla only allows “Name for Firefox”; this add-on uses neither form.
`web-ext sign --channel=listed` plus AMO metadata (name **Bookmarks API for Scripting and AI**, license MIT, privacy policy from [PRIVACY.md](../PRIVACY.md)). Listing copy: local scripts only, loopback HTTP, bearer token, bookmarks allowlist. Do not title it “Firefox …”. Mozilla only allows “Name for Firefox”; this add-on uses neither form.
+19 -3
View File
@@ -10,7 +10,6 @@ import sys
import threading
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlparse
@@ -24,7 +23,23 @@ HOST = "127.0.0.1"
PORT = int(os.environ.get("FAB_PORT", "17634"))
MAX_BODY = 256 * 1024
ALLOWED_HOSTS = {f"127.0.0.1:{PORT}", f"localhost:{PORT}"}
STATE_DIR = Path(os.environ.get("LOCALAPPDATA", str(Path.home()))) / "firefox-agent-bridge"
ALLOWED_METHODS = frozenset(
{
"meta.ping",
"meta.methods",
"bookmarks.getTree",
"bookmarks.getSubTree",
"bookmarks.getChildren",
"bookmarks.get",
"bookmarks.search",
"bookmarks.create",
"bookmarks.update",
"bookmarks.move",
"bookmarks.remove",
"bookmarks.removeTree",
"bookmarks.getRecent",
}
)
STDIN_LOCK = threading.Lock()
PENDING: dict[str, tuple[threading.Event, dict[str, Any]]] = {}
PENDING_LOCK = threading.Lock()
@@ -82,6 +97,8 @@ def verify_token_with_extension(token: str, timeout: float = 8.0) -> dict[str, A
def call_extension(method: str, args: list[Any] | None = None, timeout: float = 15.0) -> dict[str, Any]:
if method not in ALLOWED_METHODS:
raise RuntimeError(f"method not allowed: {method}")
req_id = str(uuid.uuid4())
event = threading.Event()
slot: dict[str, Any] = {}
@@ -247,7 +264,6 @@ def stdin_loop() -> None:
def main() -> int:
STATE_DIR.mkdir(parents=True, exist_ok=True)
threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start()
server = ThreadingHTTPServer((HOST, PORT), Handler)
log(f"listening on http://{HOST}:{PORT}")
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Call Firefox Agent Bridge with a Bearer token from FAB_TOKEN or a file."""
"""Call Bookmarks API with a Bearer token from FAB_TOKEN or a file."""
from __future__ import annotations
import argparse
@@ -62,7 +62,7 @@ def call(
def main() -> int:
parser = argparse.ArgumentParser(description="Call Firefox Agent Bridge")
parser = argparse.ArgumentParser(description="Call Bookmarks API")
parser.add_argument("method", help="e.g. bookmarks.search or meta.methods")
parser.add_argument("args_json", nargs="?", default="[]", help="JSON array of arguments")
parser.add_argument("--url", default=DEFAULT_BASE)
+1 -1
View File
@@ -1,5 +1,5 @@
# Register the native messaging host for the current Windows user.
# Does not load the Firefox extension — see README / AGENTS.md.
# Does not load the add-on — see README / AGENTS.md.
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env python3
"""Secrets are issued in the add-on UI, not from this script."""
from __future__ import annotations
import sys
print(
"Generate and revoke clients in Firefox: Bookmarks API toolbar → Manage clients.",
file=sys.stderr,
)
raise SystemExit(2)