Files

488 lines
22 KiB
Markdown

# sovereign_browser
A Linux x86 web browser built in C99 on WebKitGTK, designed around
**sovereign identity** instead of the traditional web's permissioned
infrastructure.
The thesis: the browser's security model (DNS/domains, TLS/CAs, same-origin
policy, CORS, cookie sandboxing) is what forces you to rely on permissioned
domains, certificate authorities, and centralized account systems. By
deprecating that model and replacing it with **Nostr identity** and **FIPS
mesh transport**, you get a browser that's more capable, not less safe —
because trust moves to the layer where it belongs: your keys.
> "Not your keys, not your browser." Walk up to a computer with your Nostr
> identity (via [n_signer](https://github.com/laantungir/n_signer)) and the
> browser is yours — your identity, your relays, your mesh.
## Goals
1. **A real browser.** Loads any normal HTTP/HTTPS page via WebKitGTK.
2. **FIPS addresses are first-class.** `http://<npub>.fips/` and `fips://`
resolve over the [FIPS](https://github.com/jmcorgan/fips) mesh, not DNS/IP.
Reach any FIPS node by its Nostr npub, no domain, no public IP, no TLS cert.
3. **Built-in Nostr signing.** The browser injects `window.nostr` (the
[nos2x](https://github.com/fiatjaf/nos2x) surface) into every page, backed
by [n_signer](https://github.com/laantungir/n_signer) — a foreground,
RAM-only, human-attended signing program. The browser never holds your
private key; every signature is approved at the signer's terminal.
4. **Deprecated web security, deliberately.** Same-origin policy, CORS, and
certificate enforcement are stripped so pages (and the agent runtime to
come) can freely call any endpoint — including FIPS mesh services — without
the workarounds traditional browsers force on automators. Security moves
up to the **Qube level** (isolation per VM), so the browser doesn't need
the traditional in-browser security model.
5. **Local file browsing.** Open and operate multipage websites directly from
`file://` with no web server — cross-origin access between local files,
`fetch()`/XHR, iframes, storage, media playback, all work. See
[Local file browsing](#local-file-browsing) below.
## Nostr interaction policy
sovereign_browser interacts with Nostr via **discrete events only** — no
continuous WebSocket subscriptions. The browser fetches events on demand
(via `relay_fetch.c`) and publishes events on demand (via `settings_sync.c`,
`agent_conversations.c`, `agent_skills.c`). There is no persistent
subscription that receives live updates.
This is a deliberate architectural choice:
- **Simplicity** — No subscription lifecycle management, no reconnection
logic, no event deduplication across reconnects.
- **Predictability** — The user controls when data is fetched or published.
No background traffic, no surprise events arriving.
- **Resource efficiency** — No open WebSocket connections consuming memory
and bandwidth while idle.
**Trade-off:** The browser does not receive live updates. If a conversation
or skill is created in another app (e.g., the client web app), it won't
appear in sovereign_browser until the user explicitly refreshes. UIs that
need fresh data provide **Refresh buttons** that trigger a one-shot fetch.
### What this means for the chat page
The `sovereign://agents/chat` page differs from `~/lt/client/www/ai.html`
in this respect:
- **ai.html** uses NDK with persistent subscriptions — conversations and
skills appear live as they're published to relays.
- **sovereign_browser** fetches conversations and skills on demand — the
user clicks a Refresh button to pull new data from relays.
Conversations and skills are still stored as the same Nostr event kinds
(30078 for conversations, 31123 for skills) with the same tags, so they're
**compatible** across projects — just not live-synced.
## Current status
Working browser with a broad feature set:
- **Multi-tab browsing** with session restore, tab drag/reorder, context menus
- **Nostr login** (GTK dialog or CLI flags) with multiple methods: generate,
local (nsec), seed (BIP-39), readonly (npub), NIP-46, n_signer hardware
- **`window.nostr` injection** (NIP-07) into every page
- **Agent MCP server** — 100 tools for browser automation via Streamable HTTP
at `http://localhost:17777/mcp`
- **Local file browsing** — open and operate multipage websites directly from
`file://` with no web server (see below)
- **Bookmarks, history, search** — built-in bookmark manager, browsing history,
and search engine integration
- **Keyboard shortcuts** — standard browser shortcuts (Ctrl+T, Ctrl+W, Ctrl+L,
Ctrl+Tab, etc.)
- **Per-user profiles** — separate identities and settings per profile
- **Embedded web content** — `sovereign://` pages for settings, bookmarks,
profile, and agent chat/config
See [`docs/webkit-poc-findings.md`](docs/webkit-poc-findings.md) for the
friction report from the POC phase.
## Install
One-command install on Debian 13 (trixie) or similar (x86_64, WebKitGTK 4.1):
```bash
curl -fsSL https://git.laantungir.net/laantungir/sovereign_browser/raw/branch/master/install.sh | bash
```
This downloads and runs [`install.sh`](install.sh), which:
1. Installs runtime dependencies via `apt` (WebKitGTK 4.1, libsoup, curl, etc.)
2. Installs **Tor** via `apt` (used for `.onion` routing)
3. Builds and installs **FIPS** from source with `CAP_NET_ADMIN` (used for `.fips` mesh routing)
4. Downloads the latest prebuilt `sovereign_browser` binary from the [Gitea releases](https://git.laantungir.net/laantungir/sovereign_browser/releases) (falls back to a source build if no binary is available)
5. Installs a `sovereign-browser` wrapper command to `/usr/local/bin`
After install, start the browser with:
```bash
sovereign-browser start --login-method generate
```
### Install options
```bash
# Non-root install to a user-writable prefix
curl -fsSL <url> | INSTALL_PREFIX=$HOME/.local bash -s -- --yes
# Force a source build instead of downloading the prebuilt binary
curl -fsSL <url> | bash -s -- --build-from-source
# Review the script before running it
curl -fsSL <url> -o install.sh
less install.sh
bash install.sh
```
Run `bash install.sh --help` for the full option list.
### What the install requires
| Requirement | Why |
|---|---|
| Debian 13+ / Ubuntu 24.04+ (WebKitGTK 4.1) | The browser is dynamically linked against `libwebkit2gtk-4.1` |
| x86_64 | Prebuilt binary is x86-64; arm64 needs `--build-from-source` |
| `sudo` / root | For `apt install`, `setcap` on the FIPS binary, and writing to `/usr/local` |
| Internet access | To download the binary, apt packages, and the FIPS source |
Tor and FIPS are **installed by default** (not optional). If either is absent at runtime the browser degrades gracefully — clearnet still works, `.onion`/`.fips` show an error page — but the installer sets them up so everything works out of the box.
---
## Build
Requires Debian 13 (trixie) or similar with WebKitGTK 4.1 dev headers:
```bash
sudo apt install libwebkit2gtk-4.1-dev
make
./browser.sh start [url] # build + launch (detached, won't hang terminal)
```
> **Note:** Always use `./browser.sh` to start/stop the browser. Running
> `./sovereign_browser` directly will hang the terminal because the GUI
> process keeps stdout/stderr open. See `./browser.sh --help` or
> [`.roo/agents.md`](.roo/agents.md) for details.
## Command-line flags
Run `./sovereign_browser --help` for the full list. Flags are parsed before
GTK init, so they don't conflict with GTK's own options. Flags override
`~/.sovereign_browser/settings.conf` for the current run only (nothing is
written to disk).
### Browser / Startup
| Flag | Description |
|---|---|
| `[URL...]` / `--url <url>` | Open URL(s) in tabs at startup (repeatable) |
| `--new-tab-url <url>` | Override the default new-tab URL |
| `--no-session-restore` | Skip session restore for this run |
| `--session-restore` | Force session restore even if disabled |
| `--max-tabs <n>` | Override max simultaneous tabs |
| `-V, --version` | Print version and exit |
| `-h, --help` | Show help and exit |
### Agent Server
| Flag | Description |
|---|---|
| `--port <port>` | Override agent MCP server port (default 17777) |
| `--no-agent` | Disable the agent server for this run |
| `--agent` | Force-enable the agent server |
| `--agent-origin <origin>` | Allowed origin (repeatable) |
### Login (skips the GTK login dialog)
Specify `--login-method` to bypass the interactive login dialog. The method
string matches the MCP `login` tool. Method-specific flags provide
credentials.
| Flag | Description |
|---|---|
| `--login-method <m>` | `generate\|local\|seed\|readonly\|nip46\|nsigner` |
| `--nsec <nsec1...>` | (local) nsec bech32 private key |
| `--privkey <hex>` | (local) 64-char hex private key |
| `--mnemonic <words>` | (seed) BIP-39 mnemonic, quoted |
| `--account <n>` | (seed) BIP-44 account index (default 0) |
| `--npub <npub1...>` | (readonly) npub bech32 public key |
| `--pubkey <hex>` | (readonly) 64-char hex pubkey |
| `--bunker <url>` | (nip46) `bunker://` URL |
| `--nsigner-transport <t>` | (nsigner) `serial\|unix\|tcp\|qrexec` |
| `--nsigner-device <path>` | (nsigner) device path / socket / host:port / qube |
| `--nsigner-service <name>` | (nsigner) qrexec service name |
| `--nsigner-index <n>` | (nsigner) key index (default 0) |
| `--no-save-identity` | Do not persist identity to disk |
| `--login-timeout <ms>` | Override agent login timeout (GTK dialog path) |
### Diagnostics
| Flag | Description |
|---|---|
| `-v, --verbose` | Increase log verbosity (repeatable) |
| `-q, --quiet` | Suppress non-error log output |
### Examples
```bash
# Basic usage (via browser.sh — recommended)
./browser.sh start https://example.com
# Generate a fresh key, skip the login dialog, open a page
./browser.sh start --login-method generate --url https://example.com
# Local key from env, custom agent port
./browser.sh start --login-method local --nsec "$NSEC" --port 18888
# Read-only (npub), no agent server, two tabs
./browser.sh start --login-method readonly --npub npub1... \
--no-agent --url https://a.com --url https://b.com
# Open a local website (no web server needed)
./browser.sh start --login-method generate \
--url "file:///path/to/website/index.html"
```
## Architecture (summary)
```
┌─────────────────────────────────────────────────────┐
│ sovereign_browser host (C99) │
│ ┌─────────┐ ┌──────────┐ ┌────────────────────┐ │
│ │ UI / │ │ Request │ │ Nostr signer shim │ │
│ │ tabs │ │ router │ │ (nostr_core_lib → │ │
│ │ URL bar │ │ │ │ n_signer socket) │ │
│ └─────────┘ └────┬─────┘ └─────────┬──────────┘ │
│ │ │ │
│ ┌──────────┼───────────┬───────┘ │
│ ▼ ▼ ▼ │
│ http/https fips:// nostr:// │
│ (WebKit net) (FIPS TUN) (relays) │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ WebKitGTK (system lib, ~35 MB) │
│ WebCore renderer + JavaScriptCore + libsoup net │
└─────────────────────────────────────────────────────┘
```
Full architecture, engine comparison, and roadmap in
[`docs/architecture.md`](docs/architecture.md).
### Custom URI scheme layer
The browser registers custom URI schemes via
`webkit_web_context_register_uri_scheme()`. This is the **network/content
layer** — it controls what bytes get loaded into the web view. One
infrastructure, multiple uses:
| Scheme | Purpose | Status |
|--------|---------|--------|
| `sovereign://nostr/*` | `window.nostr` bridge — web pages call `fetch('sovereign://nostr/signEvent')` to sign via the C-side `nostr_signer_t` | ✅ Working |
| `sovereign://settings`, `sovereign://bookmarks`, `sovereign://profile` | Browser-internal pages (like `chrome://settings`) | ✅ Working |
| `sovereign://agents/chat`, `sovereign://agents/config` | Agent chat and configuration pages | ✅ Working |
| `fips://` / `*.fips` | Route to FIPS mesh nodes via TUN interface | Planned |
| `nostr://` | Fetch Nostr events from relays, render as HTML | Planned |
A `WebKitWebExtension` (separate `.so` loaded into the web process) will be
added later for the **script manipulation layer** — security stripping
(CORS/SOP removal), synchronous NIP-07 calls, and content injection. The URI
scheme and WebExtension serve different layers and will coexist. See
[`plans/nostr-login-integration.md`](plans/nostr-login-integration.md) for the
full decision rationale.
### Nostr login
Login is a native GTK dialog (not a web page) that calls
[`nostr_core_lib`](https://github.com/laantungir/nostr_core_lib) directly.
Supported methods:
| Method | nostr_core_lib API |
|--------|-------------------|
| Local key (nsec) | `nostr_signer_local()`, `nostr_decode_nsec()` |
| Seed phrase (BIP-39) | `nostr_derive_keys_from_mnemonic()` |
| Read-only (npub) | `nostr_decode_npub()` |
| NIP-46 remote signer | `nostr_nip46_client_session_init()` |
| n_signer hardware | `nostr_signer_nsigner_serial/unix/tcp/qrexec()` + `nostr_signer_nsigner_set_nostr_index()` |
See [`plans/nostr-login-integration.md`](plans/nostr-login-integration.md) for
the full implementation plan.
### Browser tabs
The browser supports multiple tabs via a `GtkNotebook`. Each tab has its own
toolbar (hamburger menu + URL entry) and `WebKitWebView`. All tabs share a
single `WebKitWebContext`, so the `sovereign://` Nostr bridge, security
settings, and TLS policy apply to every tab automatically.
**Tab features:**
- New tab: `Ctrl+T` or the `+` button at the end of the tab strip
- Close tab: `Ctrl+W`, per-tab close button, or middle-click on the tab label
- Switch tabs: `Ctrl+Tab` / `Ctrl+Shift+Tab`, `Ctrl+PageUp` / `Ctrl+PageDown`
- Focus URL bar: `Ctrl+L`
- Right-click tab context menu: New Tab, Close Tab, Close Other Tabs, Close
Tabs to the Right, Duplicate Tab, Reload Tab
- Drag tabs to reorder
- Tab titles update from page load
- Session save/restore: open tabs are saved on exit and restored on next
launch (configurable in Settings)
**Settings dialog** (hamburger menu → Settings…):
- Restore session on startup (default: on)
- New tab URL (default: `https://example.com`)
- Tab bar position: Top / Bottom / Left / Right (default: Top)
- Show tab close buttons (default: on)
- Middle-click to close tab (default: on)
- Ctrl+Tab to switch tabs (default: on)
- Maximum tabs (default: 50)
- Allow drag to reorder tabs (default: on)
Settings are persisted to `~/.sovereign_browser/settings.conf`. Session state
is saved to `~/.sovereign_browser/session.txt`.
See [`plans/browser-tabs.md`](plans/browser-tabs.md) for the full
implementation plan.
### Local file browsing
sovereign_browser is designed to run websites directly from local files —
open an `index.html` from its directory and navigate as if it were served
over HTTP. No web server required. This is a deliberate design choice:
security is moved up to the Qube level, so the browser doesn't need the
traditional security restrictions (same-origin policy, CORS) that would
block local-file web apps.
**What works on `file://` pages:**
- **Relative links** between pages — `index.html``about.html`,
`subdir/page.html`, `../index.html`, `../../../deep/page.html`
- **Cross-origin access** — all `file://` URLs are treated as same-origin.
Iframes can access their parent's DOM, `window.open()` popups are
accessible, and `fetch()`/XHR work across directories.
- **`fetch()` and `XMLHttpRequest`** — load local JSON, text, and other
files via relative paths (returns `status=0`, the `file://` convention)
- **External resources** — CSS stylesheets, JavaScript files, SVG images,
favicons, `<picture>`/`srcset`, `<object>`/`<embed>`
- **Video and audio playback** — MP4 video and M4A audio play correctly
with full duration/dimension metadata
- **All browser storage** — `localStorage`, `sessionStorage`, `cookies`,
and `IndexedDB` all work fully (open, upgrade, put, get, count)
- **`history.pushState`/`replaceState`** — SPA-style routing works on
`file://` URLs
- **Query strings and hash fragments** — preserved in `location.href`,
accessible via `location.search`/`location.hash`
- **URL-encoded filenames** — `encoded%20name.html` (file on disk:
`encoded name.html`) loads correctly
- **Form submissions** — GET forms append query strings to the target
page; POST forms navigate (body silently dropped, no server)
- **`window.open()`** — opens local pages in new tabs
- **ES Modules** — dynamic `import()` works with local module files
- **Web Workers and Shared Workers** — `new Worker()` and
`new SharedWorker()` with local scripts enable background computation
threads (including cross-tab shared workers)
- **WebAssembly** — `WebAssembly.instantiate()` works for compiled code
- **Cross-origin fetch to remote HTTPS** — `fetch('https://example.com')`
from a `file://` page works with no CORS blocking (confirms the
"deprecated web security" design goal)
- **Cross-origin remote resources** — remote `<script src>`,
`<link rel=stylesheet>`, and `<img>` from HTTPS CDNs all load correctly
**Known limitations (WebKit engine quirks, not bugs):**
- `fetch()` for missing files throws `Load failed` instead of returning an
error Response (no HTTP status codes on `file://`)
- `<video>`/`<audio>` `error` events don't fire for missing source files
(valid media works perfectly)
- Cache API (CacheStorage) rejects non-HTTP/HTTPS URLs — use IndexedDB
instead for local-file storage
- Service Workers don't work on `file://` (W3C spec requires HTTP/HTTPS) —
use Web Workers for background processing instead
**Test site:** A comprehensive multipage test website with 53 edge cases
is in [`tests/local-site/`](tests/local-site/index.html). See
[`tests/local-site/LOCAL-FILE-BROWSING-REPORT.md`](tests/local-site/LOCAL-FILE-BROWSING-REPORT.md)
for the full test report.
```bash
# Open a local website
./browser.sh start --login-method generate \
--url "file:///path/to/your/website/index.html"
```
### Agent tools (MCP server)
The browser embeds an MCP (Model Context Protocol) server using Streamable
HTTP transport. External AI agents can control the browser programmatically
via `http://localhost:17777/mcp`.
The server starts before the login dialog and remains available throughout
the browser's lifecycle. An agent can log in at any time: while the dialog
is showing, or after the browser is already running.
**100 tools available**, including:
- **Login**: `login_status`, `login`, `logout`, `switch_identity`
- **Navigation**: `open`, `back`, `forward`, `reload`, `get_url`, `get_title`
- **Snapshot & inspection**: `snapshot`, `get_text`, `get_html`, `get_attr`,
`eval`, `screenshot`, `screenshot_annotated`
- **Interaction**: `click`, `dblclick`, `fill`, `type`, `press`, `scroll`,
`hover`, `focus`, `select`, `check`, `uncheck`, `drag`
- **Find elements**: `find_role`, `find_text`, `find_label`, `find_placeholder`,
`find_alt`, `find_title`, `find_testid`
- **Tabs**: `tab_list`, `tab_new`, `tab_switch`, `tab_close`, `close`, `close_all`
- **Wait**: `wait`, `wait_for`, `wait_for_text`, `wait_for_url`, `wait_for_load`
- **Cookies & storage**: `cookies_get/set/clear`, `storage_local_*`, `storage_session_*`
- **Frames**: `frame_switch`, `frame_main`
- **Dialogs**: `dialog_accept`, `dialog_dismiss`, `dialog_status`
- **Debug**: `console`, `errors`, `highlight`
- **Files**: `upload`, `pdf`
**Example workflow:**
```bash
# 1. Login (or use --login-method on the command line)
curl -s -X POST http://localhost:17777/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"login","arguments":{"method":"generate"}}}'
# 2. Open a page
curl -s -X POST http://localhost:17777/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"open","arguments":{"url":"https://example.com"}}}'
# 3. Snapshot — get accessibility tree with element refs
curl -s -X POST http://localhost:17777/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"snapshot","arguments":{}}}'
# 4. Click by ref
curl -s -X POST http://localhost:17777/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"click","arguments":{"ref":"@e2"}}}'
```
See [`.roo/agents.md`](.roo/agents.md) for the full tool list and workflow
guide, and [`plans/agent-tools.md`](plans/agent-tools.md) for the
implementation plan.
## Roadmap
1. ✅ Base browser (WebKitGTK + C99, loads pages)
2. ✅ Browser tabs (GtkNotebook, per-tab toolbar, session restore, settings)
3. ✅ Nostr login (GTK dialog → nostr_core_lib → nostr_signer_t, 6 methods)
4.`window.nostr` injection (`sovereign://` URI scheme bridge)
5. ✅ Agent MCP server (100 tools, Streamable HTTP at `http://localhost:17777/mcp`)
6. ✅ Embedded web content (`sovereign://settings`, `bookmarks`, `profile`, `agents/chat`, `agents/config`)
7. ✅ Bookmarks, history, search, keyboard shortcuts, per-user profiles
8. ✅ Local file browsing (multipage websites from `file://` with no web server)
9. ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
10. ⏳ FIPS URI scheme (`fips://` / `*.fips` via WebKitGTK scheme handler)
11.`nostr://` content scheme
12. Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
## License
MIT (to match nostr_core_lib / n_signer / didactyl).