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` 2. Native host installed only if scripts should call in: `powershell -NoProfile -File tools/install-native-host.ps1`
3. Firefox **running** for those calls 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 ## Auth
This is a normal API token, not an SSH key. This is a normal API token, not an SSH key.
1. In Firefox, open the Bookmarks API toolbar icon → **Manage clients**. 1. In Firefox, open the Bookmarks API toolbar icon → **Manage clients**.
2. Name the client (e.g. `cursor-agent`) and click **Generate secret**. 2. Name the client 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. 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: 4. Give tooling only that string:
```text ```text
@@ -41,7 +41,7 @@ python tools/client.py METHOD [ARGS_JSON]
`ARGS_JSON` is a JSON **array** matching the WebExtension function arguments. `ARGS_JSON` is a JSON **array** matching the WebExtension function arguments.
```text ```text
python tools/client.py bookmarks.search "[{\"title\":\"10.132.x.x\"}]" python tools/client.py bookmarks.search "[{\"title\":\"Research\"}]"
``` ```
HTTP: HTTP:
@@ -51,10 +51,10 @@ POST /v1/call
Authorization: Bearer fab_ Authorization: Bearer fab_
Content-Type: application/json 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 | | Method | Args | Notes |
|--------|------|--------| |--------|------|--------|
@@ -72,7 +72,7 @@ Content-Type: application/json
| `bookmarks.removeTree` | `id` | Folder and descendants | | `bookmarks.removeTree` | `id` | Folder and descendants |
| `bookmarks.getRecent` | `numberOfItems` | | | `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). 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 | | 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 `Bearer token required` | Unsigned request | Set `FAB_TOKEN` |
| 401 `unknown token` | Revoked, typo, or never generated | Manage clients → generate again | | 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 | | Temporary add-on gone after restart | Unsigned on Firefox Release | Load again, or sign via AMO |
## Security boundaries ## Security boundaries
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 EasyGoin / EGG Copyright (c) 2026 EasyGoin
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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 # 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)). 1. Load the add-on.
2. Toolbar → **Manage clients** → generate a secret → store it with your secrets. 2. Toolbar → **Manage clients** → generate a secret → store that secret yourself.
3. To let scripts call in, register the native host once (`tools/install-native-host.ps1` on this workstation). 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 ## Security
- Binds **127.0.0.1** only. Browser `Origin` headers are rejected. - HTTP binds **127.0.0.1** only. Requests with a browser `Origin` header are rejected.
- Client hashes live in the add-ons storage. The host asks the add-on whether a token is valid. - The native host may talk only to this add-ons gecko id.
- Only `bookmarks.*` (plus `meta.*`). No history, cookies, tabs, or `onMessageExternal`. - 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 ## 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 # 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. 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). 1. Register at [addons.mozilla.org](https://addons.mozilla.org/) (Mozilla account).
2. Open [API credentials](https://addons.mozilla.org/developers/addon/api/key/). 2. Open [API credentials](https://addons.mozilla.org/developers/addon/api/key/).
3. Generate a JWT **issuer** (`user:…`) and **secret**. 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 ## 2. Choose a channel
| Channel | What you get | | Channel | What you get |
|---------|----------------| |---------|----------------|
| **unlisted** | Signed `.xpi` for self-install. Not on the AMO store. Enough to survive Firefox restarts. **Start here.** | | **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`. 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 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. Keep the native host registered (`tools/install-native-host.ps1`). Signing replaces only the extension; the host is unchanged.
## 4. Updates ## 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) ## 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 threading
import uuid import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
@@ -24,7 +23,23 @@ HOST = "127.0.0.1"
PORT = int(os.environ.get("FAB_PORT", "17634")) PORT = int(os.environ.get("FAB_PORT", "17634"))
MAX_BODY = 256 * 1024 MAX_BODY = 256 * 1024
ALLOWED_HOSTS = {f"127.0.0.1:{PORT}", f"localhost:{PORT}"} 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() STDIN_LOCK = threading.Lock()
PENDING: dict[str, tuple[threading.Event, dict[str, Any]]] = {} PENDING: dict[str, tuple[threading.Event, dict[str, Any]]] = {}
PENDING_LOCK = threading.Lock() 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]: 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()) req_id = str(uuid.uuid4())
event = threading.Event() event = threading.Event()
slot: dict[str, Any] = {} slot: dict[str, Any] = {}
@@ -247,7 +264,6 @@ def stdin_loop() -> None:
def main() -> int: def main() -> int:
STATE_DIR.mkdir(parents=True, exist_ok=True)
threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start() threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start()
server = ThreadingHTTPServer((HOST, PORT), Handler) server = ThreadingHTTPServer((HOST, PORT), Handler)
log(f"listening on http://{HOST}:{PORT}") log(f"listening on http://{HOST}:{PORT}")
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 from __future__ import annotations
import argparse import argparse
@@ -62,7 +62,7 @@ def call(
def main() -> int: 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("method", help="e.g. bookmarks.search or meta.methods")
parser.add_argument("args_json", nargs="?", default="[]", help="JSON array of arguments") parser.add_argument("args_json", nargs="?", default="[]", help="JSON array of arguments")
parser.add_argument("--url", default=DEFAULT_BASE) 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. # 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" $ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot $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)