15 KiB
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) and the browser is yours — your identity, your relays, your mesh.
Goals
- A real browser. Loads any normal HTTP/HTTPS page via WebKitGTK.
- FIPS addresses are first-class.
http://<npub>.fips/andfips://resolve over the FIPS mesh, not DNS/IP. Reach any FIPS node by its Nostr npub, no domain, no public IP, no TLS cert. - Built-in Nostr signing. The browser injects
window.nostr(the nos2x surface) into every page, backed by 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. - 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.
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.
Non-goals (for now)
- Agent integration, multi-window agent hosts, didactyl hosting, and the
broader "browser as agent runtime" vision are documented in
docs/architecture.mdand the linked plans, but not in scope for the first build. First: a usable browser with basic Nostr signing. Then the rest.
Current status
Working browser: a C99 + WebKitGTK window with multi-tab support, a URL bar
per tab, Nostr login, and window.nostr injection. Verified loading
https://laantungir.net cleanly. See
docs/webkit-poc-findings.md for the friction
report from the POC phase (and the Servo fallback exploration).
[loaded] https://laantungir.net/ -- title: Laan Tungir
Build
Requires Debian 13 (trixie) or similar with WebKitGTK 4.1 dev headers:
sudo apt install libwebkit2gtk-4.1-dev
make
./sovereign_browser [url]
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
# Existing behavior — still works
./sovereign_browser https://example.com
# Generate a fresh key, skip the login dialog, open a page
./sovereign_browser --login-method generate --url https://example.com
# Local key from env, custom agent port
./sovereign_browser --login-method local --nsec "$NSEC" --port 18888
# Read-only (npub), no agent server, two tabs
./sovereign_browser --login-method readonly --npub npub1... \
--no-agent --url https://a.com --url https://b.com
# Via browser.sh (forwards extra args)
./browser.sh start --login-method generate --url https://example.com
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) │
│ Blink-grade renderer + V8 + libsoup network stack │
└─────────────────────────────────────────────────────┘
Full architecture, engine comparison, and roadmap in
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 |
Planned |
sovereign://settings |
Browser-internal pages (like chrome://settings) |
Planned |
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 for the
full decision rationale.
Nostr login
Login is a native GTK dialog (not a web page) that calls
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 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+Tor 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 for the full
implementation plan.
Agent tools
The browser embeds a WebSocket server (using libsoup) that allows external
AI agents to control the browser programmatically. Connect to
ws://localhost:17777/agent and send JSON tool commands.
The server starts before the login dialog and remains available throughout the browser's lifecycle. The browser starts normally with the GTK login dialog — no blocking wait. An agent can log in at any time: while the dialog is showing, or after the browser is already running. If an agent logs in while the dialog is open, the agent's login takes priority.
Login tools (available before login):
login_status— check if logged inlogin— authenticate with method:local(nsec),seed(BIP-39 mnemonic),readonly(npub),nip46(bunker:// URL), ornsigner(hardware signer via serial/unix/tcp/qrexec transport)logout— clear signer and reset stateswitch_identity— change identity (same params as login)
Browser tools (available after login):
- Navigation:
open,back,forward,reload,get_url,get_title - Inspection:
snapshot(accessibility tree with element refs),get_text,get_html,get_attr,eval(run JavaScript) - Interaction:
click,fill,type,press,scroll,hover,focus,close - Tabs:
tab_list,tab_new,tab_switch,tab_close - Wait:
wait(ms),wait_for(selector)
Snapshot + ref workflow (same pattern as agent-browser):
# Using websocat (cargo install websocat)
# 1. Login
echo '{"id":1,"tool":"login","params":{"method":"readonly","npub":"npub1..."}}' | websocat ws://localhost:17777/agent
# 2. Open a page
echo '{"id":2,"tool":"open","params":{"url":"https://example.com"}}' | websocat ws://localhost:17777/agent
# 3. Snapshot — get accessibility tree with refs
echo '{"id":3,"tool":"snapshot","params":{"interactive":true}}' | websocat ws://localhost:17777/agent
# 4. Click by ref
echo '{"id":4,"tool":"click","params":{"ref":"@e2"}}' | websocat ws://localhost:17777/agent
HTTP status endpoint: curl http://localhost:17777/ returns server
status (running, port, client count, logged in).
Agent settings (in Settings dialog or ~/.sovereign_browser/settings.conf):
agent_server_enabled(default: true)agent_server_port(default: 17777)agent_allowed_origins(default:*)agent_login_timeout_ms(default: 30000)
See plans/agent-tools.md for the full
implementation plan and tool comparison with agent-browser.
Roadmap
- ✅ Base browser (WebKitGTK + C99, loads pages)
- ✅ Browser tabs (GtkNotebook, per-tab toolbar, session restore, settings)
- ✅ Agent tools Phase 1 (WebSocket server, login tools, 31 browser automation tools)
- ⏳ Nostr login (GTK dialog → nostr_core_lib → nostr_signer_t)
- ⏳
window.nostrinjection (sovereign://URI scheme bridge) - ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
- ⏳ FIPS URI scheme (
fips:///*.fipsvia WebKitGTK scheme handler) - ⏳
nostr://content scheme - ⏳ Agent tools Phase 2 (refinements) and Phase 3 (extended tool catalog)
- Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
License
MIT (to match nostr_core_lib / n_signer / didactyl).