Gateway

Persistent local service. Runs alongside the browser as a separate Rust process. Hosts the agent harness, polls feeds, watches files, runs scheduled monitors, and serves the local HTTP API.

Why a separate process

If the agent only ran in the browser process, closing the window would stop feed polling, halt monitors, and silence the agent. The gateway exists so a closed browser doesn’t mean a stopped agent.

Gateway vs CDP BackgroundService

The word “background” shows up in two distinct places in Egg, and they don’t overlap. The gateway runs persistent services for tasks Egg itself orchestrates: poll feeds, check monitored pages, watch directories, dispatch notifications, run cron-driven jobs, drive multi-device sync. These are processes Egg composes to do work on the user’s behalf, regardless of whether the browser is open.

The Chrome DevTools Protocol exposes a separate BackgroundService domain that surfaces what web pages themselves register and run inside the engine: service workers, Background Fetch, Background Sync, Push Messaging, Notifications. This is an observability and debugging surface for in-page activity, not a place to schedule work. A site declares a service worker; the engine runs it; the BackgroundService domain lets a developer see when and what.

The line is: gateway services do work for you; BackgroundService tells you what work a page is doing inside the engine. A page monitor that polls a URL on a 30-minute cadence is a gateway service, with Egg owning the schedule, the fetch, the comparison, and the alert. A push subscription registered by a web page lives entirely inside the engine, with Egg able to observe it through CDP if you ask, but not own its lifecycle.

When to reach for which:

You want to…Use
Schedule recurring work that survives browser closureGateway scheduler
Poll a URL, diff results, fire an alertGateway monitor_checker
Pull from RSS, HN, GitHub feeds and build a digestGateway poller + digest
Watch a directory for filesystem eventsGateway watcher
Sync data across paired devicesGateway sync engine
See what service workers a site has registeredCDP BackgroundService via the browser API’s CDP passthrough
Debug Background Fetch downloads or push subscriptions on a tabCDP BackgroundService
Stream service-worker lifecycle events as they fire on a pageCDP BackgroundService

The two surfaces also differ in lifetime. Gateway services run continuously inside egg-daemon, so they survive browser exit, system suspend (within reason), and tab churn. BackgroundService observations are scoped to a tab’s engine context: the moment that tab is closed, the observer goes with it. Nothing the gateway does relies on a tab being open; nothing the BackgroundService domain shows you persists past a tab.

Architecture

Browser and gateway are siblings, not parent-and-child. They communicate over a local HTTP/WebSocket bridge and coordinate state through SQLite WAL.

Two responsibilities sit at the architectural center: hosting the agent loop with full LLM provider access, and running the hidden engine fleet that scrapes pages too dynamic for an HTTP fetcher. The remaining modules (feed polling, scheduling, sync, notifications, the tray, the API server) are scaffolding around those two.

User's machine Egg Browser Tauri app React shell Tab webviews Address bar Reader Browser API Egg Gateway egg-daemon, system tray Agent & LLM management conversation loop, tool dispatch, plan approval provider routing, key custody, streaming Headless WebView2 fleet hidden engine instances driven via CDP spawn, navigate, scrape, tear down Collection & coordination poller, digest monitor_checker watcher, scheduler alerts, tray Bridge & transport browser_rpc (WS) api (Axum HTTP) sync engine credentials egg.db (SQLite WAL) WebSocket LLM providers Anthropic, OpenAI, local models Target sites scraped via headless fleet Web feeds RSS, HN, Reddit, GitHub Egg Cloud relay opaque ciphertext only

Services

Each module owns one job. They share the database and the HTTP server, and run on a single tokio runtime.

Agent & LLM management

This is the gateway’s primary purpose. Every chat turn, every tool call, every plan approval, every model invocation runs here. The browser never makes an LLM request directly; it streams from the gateway. Provider routing, API-key custody, retry and rate-limit handling, audit logging, and background thinking all live in this layer so the agent can keep working when no window is open.

The conceptual agent model (turn loop, autonomy, skills, tools, firewalls, memory, free agents, LLM routing) is covered on the Agents page. The table below lists the modules that host that work inside the gateway.

ModuleJob
gateway_agentThe agent harness. Owns the conversation loop, LLM dispatch, tool catalog, plan-approval gating, audit log.
agent_apiHTTP handlers for chat streaming, AG-UI, plan approval.
background_thinkingLong-running self-directed sessions. Records reflections and feeds the curiosity queue.
reflectionIdentity, personality, and reflection state.
reservationsPrivate time, time-blocks, soft-plan, materialize-now, plan-now.
ai_apiThe /api/ai/* family. One endpoint per modality (complete, stream, embed, transcribe, speech-recognize, tts, vision, generate-image, video-generate). Routes through capability resolver and tier gate.
media_apiThe /api/media/* family. ffmpeg, ffprobe, yt-dlp, whisper.cpp model management. Path-validated to app data / tmp / downloads roots.
registryCapability and asset registry. Local assets (Ollama models, whisper installs), cloud capability seeds, model-capability mappings. Single writer.
tier_gateDaily-cap enforcement per capability + tier. Canonical writer of capability_usage.
llm_apiInternal /x/llm/* endpoints used by the in-tree egg-llm proxy. Skip the tier gate; not part of the public contract.
provider_credentialsProvider credential vault. CRUD over llm_providers. Read responses redact secret material.
credentialsGeneric 3rd-party API credentials (non-LLM).

Browser bridge

ModuleJob
browser_rpcWebSocket calls to the running browser for tool dispatch (clicks, navigation, eval).

Headless WebView2 fleet

Some target pages can’t be handled by an HTTP fetcher. They require a real engine to execute JavaScript, hold cookies and session state, render dynamic content, or pass anti-bot heuristics. The gateway owns a fleet of hidden engine instances for that work.

Each instance is a headless WebView2 process spawned by the gateway, driven over CDP, and torn down when the job completes. The windows never reach the screen. The fleet is what backs page monitors that target script-heavy URLs, social-feed collection (Twitter/X timelines, Reddit threads behind a wall, LinkedIn posts), and any agent tool call that needs to interact with a page rather than just fetch it.

Lifecycle is the gateway’s responsibility, not the browser’s. Tabs open in the user-facing browser are unaffected by what the fleet is doing; the fleet’s engine instances are isolated processes with their own data stores. A monitor checking ten sites every fifteen minutes might cycle ten short-lived engine instances per round and leave nothing behind.

Data collection

ModuleJob
pollerFeed-polling loop. RSS, Atom, HN, Reddit, GitHub, custom sources. ~15-minute default cadence.
scoringRule-based item relevance (0–100). Keywords, authors, domains.
digestRolling digest builder.
correlationCross-feed pattern detection: the same story breaking on multiple sources.
monitor_checkerPage monitors. Schedules URL checks, diffs results, writes Markdown reports.
watcherFilesystem watcher for workspaces and the agent’s working folder.

Notification & coordination

ModuleJob
schedulerCron-based task scheduling for monitors, polls, digest builds.
alertsDesktop notifications and external channels (email, Telegram, push).
trayWindows system-tray icon and menu.
apiThe Axum HTTP server. ~170 routes across AI, media, fetch, privacy, agent, feeds, monitors, credentials, sync, lifecycle, plus internal /x/* surfaces.
public_apiThe versioned /api/v1/* surface (fetch/page, fetch/image, privacy/hibp) plus /api/v1/openapi.json. The public-contract entry point.
private_apiInternal /x/* endpoints: skills, extensions, search, images, GCS storage, Egglet runtime hooks.
cloud_relayEgg Cloud egress. Typed families (agent-calls, pairing, group-chat, dm, notifications, etc.) plus a generic passthrough for one-off paths.
cloud_commandsInbound command queue from Egg Cloud. Polls upstream, queues locally, lets the browser drain via /x/cloud-commands/pending, ships results back.
filter_listsEasyList / EasyPrivacy 24-hour refresh task. Runs only when aggressive ad-blocking is enabled.
net_safetySSRF guard. Every caller-supplied URL passes through here: rejects loopback, RFC1918, link-local, CGNAT, ULA, multicast, mDNS; resolves DNS to defeat rebinding.

Lifecycle

Single-instance. The browser auto-launches it, or run it yourself.

# Boot in foreground (logs to stdout)
egg-daemon

# Inspect the running instance
egg-daemon --status

# Terminate it
egg-daemon --kill

# Pin port + token for development
egg-daemon --port=19999 --token=devtoken

Calling it

Bearer token from daemon_info.json. JSON over HTTP. Streaming endpoints use Server-Sent Events. Full route list on the HTTP API page.

# Read the digest as JSON
curl -s "http://127.0.0.1:$PORT/digest" \
  -H "Authorization: Bearer $TOKEN"

# Force-poll every feed
curl -s -X POST "http://127.0.0.1:$PORT/feeds/poll-all" \
  -H "Authorization: Bearer $TOKEN"

# Stream sync events
curl -N "http://127.0.0.1:$PORT/sync/events" \
  -H "Authorization: Bearer $TOKEN"

Cross-device sync

Two devices running Egg can keep the user’s bookmarks, passwords, settings, and a small set of related categories in step without the server ever seeing the plaintext. The cloud relay’s only job is to forward opaque encrypted bytes between paired daemons. It can read addressing metadata (which pseudonym is talking to which), but never bookmark URLs, password vaults, settings values, or anything else inside the envelope.

The whole pipeline is event-driven: a local write triggers the push, not a clock. There is no polling loop deciding when to sync.

What gets synced

Two channels run in parallel.

Continuous. Every local write to bookmarks, passwords, passkeys, settings, or firewall rules is mirrored to every paired peer. Deletes propagate too: each of those categories has a database trigger that records a tombstone the moment a row is removed, so the next push round ships the deletion alongside any inserts and updates.

One-time bulk transfer. Browse history, open tabs, and cookies/sessions can ship once on first pairing if the user opts in. They’re useful for setting up a second machine without re-logging into everything; they aren’t kept in continuous sync after the initial transfer (that would be both noisy and not what users want for tabs).

Pairing

One device generates a 6-character invite. The other types it in. Behind the scenes, both layers of pairing happen against the same code in one flow: a cloud-side authorization so the relay knows these two pseudonyms are allowed to talk, and an end-to-end key agreement.

Device A                    Cloud relay                   Device B
   |                            |                             |
   |  POST /sync/invite/create  |                             |
   |  (registers code locally   |                             |
   |   and in cloud’s pairing   |                             |
   |   index)                   |                             |
   |--------------------------->|                             |
   |                            |   user pastes 6-char code   |
   |                            |<----- POST /pair/accept ----|
   |                            |                             |
   |                            |  ----- partner_id ------>   |
   |                            |                             |
   |                            |  KEX 1: A’s X25519 pubkey   |
   |<------ relay forwards -----|                             |
   |                            |                             |
   |  KEX 2: B’s X25519 pubkey  |                             |
   |--------------------------->|------- relay forwards ---->|
   |                            |                             |
   [both sides derive shared AES-256-GCM key via
    HKDF(ECDH-shared-secret, info = invite_code)]
   |                            |                             |
   [pairings flip to active on both ends]

The invite code is mixed into the HKDF info parameter on both sides. Without it, an attacker who could substitute their own X25519 keys at the relay would land in a working ECDH but with a key only they share with one device. Mixing in the code (which never crosses the relay in cleartext beyond the initial accept round-trip) binds the derived key to the human-typed secret. Both private keys stay in their device’s keystore the whole time, never crossing the relay.

Push and pull

A SQLite update_hook is installed at boot on every synced table. When a row is inserted, updated, or deleted, the hook fires synchronously on the writing thread, signals an in-process tokio::Notify, and returns. No I/O, no locking. A separate task waits on the Notify, then sleeps ~750 ms to coalesce bursty writes (typing into a settings form, importing bookmarks) into one push round per active pairing.

A push round, per category, walks rows newer than the device’s per-partner watermark for that category, capped at a per-envelope size limit. Rows are encoded, encrypted with the per-pair AES-GCM key, and POSTed as opaque ciphertext to the relay. The relay routes by recipient pseudonym and holds the envelope until the partner’s daemon picks it up over its long-poll connection.

The receiver decrypts, validates the replay counter, and applies rows where the inbound timestamp exceeds the local one (last-writer-wins on a per-row basis). It then advances its pull watermark for that category, increments the receive-side replay counter, and emits a per-category event so the renderer refreshes that surface without a reload. Watch the bookmark bar during a sync and new entries appear with no user action.

Watermarks & recovery

Watermarks are tracked from each device’s own perspective: per partner, per category, the high-water mark of locally-modified rows already shipped, alongside the last push and last pull timestamps. The watermark is what the device has confirmed-sent, not the last thing seen from the partner.

Replay counters are tracked per partner and per direction. The receiver rejects any envelope whose counter is ≤ the previous one for that direction. This catches reorder, duplication, and any attempt by a compromised relay to replay an old ciphertext.

A successful re-pair (KEX completing again, generally after a revoke) wipes both watermarks and replay counters for that partner, so the first push after re-pair sends every row from scratch and the new counters start at zero. Without this, leftover state from a prior pairing would silently truncate the first sync; the progress dialog would say “synced” while no data went out.

Local storage

Everything sync touches is in the same local database the rest of Egg uses. There is no separate sync store and no daemon-private database. The two processes coordinate through reads and writes against this shared file. Sync state (pairings, watermarks, replay counters, pending tombstones, outstanding invites) lives there alongside the synced categories themselves. The derived per-pair AES-GCM key never sits in the database; it is held by the OS keychain and only loaded into the daemon’s memory when a sync round runs.

Threat model

The cloud relay is treated as semi-trusted infrastructure. It sees device pseudonyms, ciphertext sizes, and timing, enough to know that two devices talk and roughly how often, never what they say. AEAD (AES-256-GCM) means a compromised relay cannot forge envelopes; the worst it can do is drop, delay, or reorder, and the replay counter catches reorder and duplication.

A device that has paired is fully trusted within whatever categories the user enabled; that’s the point. If you’re worried about a peer being compromised, revoke the pairing on this device; the next push attempt from that peer will fail at the AEAD check on its way back through the relay.

There is no cloud-held key escrow. If you lose every paired device, the data on the cloud is unrecoverable ciphertext. If you lose one device’s keystore, re-pair from a surviving device. The surviving copy ships the full state to the new pairing on first push.