Present the repo as a scoped bookmarks API product and drop homelab-only notes.
This commit is contained in:
@@ -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,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
@@ -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.
|
||||
@@ -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-on’s 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-on’s 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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user