Skip to main content

Architecture

Leuria has two sides and one contract: an engine on the visitor's machine, a browser SDK in the website, and the engine protocol between them (protocol).

website (any origin) visitor's machine
┌──────────────────────────┐ HTTP + SSE ┌─────────────────────────────────────┐
│ @leuria/client │ ───────────▶ │ leuria engine 127.0.0.1:19570 │
│ Leuria (cascade, state) │ │ server.ts host/origin/token gate │
│ Conversation (turns) │ ◀─ WebSocket │ pairing.ts approval page, grants │
│ providers: │ (tool calls)│ routes.ts /session/* │
│ bridge ──────────────┐ │ │ session-manager.ts │
│ browserAI (Prompt API)│ │ │ webmcp-server.ts page tools ⇄ MCP │
│ server (site's API) │ │ │ acp/ agent process over stdio │
└─────────────────────────┴─┘ │ policy.ts what the agent may do │
└──────────────┬──────────────────────┘
│ ACP (JSON-RPC, stdio)
▼
Claude Code adapter (pinned, sandboxed)

Desktop app (apps/desktop)​

A Tauri 2 shell (Rust) around the same engine, compiled with Bun as a sidecar and started with start --app: the engine then prints JSON events on stdout and serves an admin API (admin.ts) to the app's UI, authenticated with a random token the shell generates. The UI is React with Sinux stores. See apps/desktop/README.md.

Engine (packages/engine, npm leuria)​

FileRole
cli.tsCommands: start (default), setup, doctor, test, sites, agents
server.tsThe only door: loopback bind, Host check, and caller identification. Callers are local (no Origin), self (the approval page) or site (needs a grant token)
pairing.tsA leuria://connect link, registered by the app (link()), or (CLI) the page's first claim → the visitor decides (the app, or the engine's page: decide, self origin only) → POST /connect/claim hands the token to the named origin, with the link's nonce, once
grants.tsOne grant per origin with a SHA-256 of its token; reloads when another process edits the file
routes.tsThe /session/* API; sessions are only visible to their origin
session-manager.tsSession lifecycle, a sandbox folder per session, SSE fan-out, limits
webmcp-server.tsPage tools: WebSocket from the page, MCP over HTTP for the agent
acp/acp-client.tsStarts the agent and runs the ACP handshake (initialize, authenticate, session/new), then prompts, cancels, streams updates and answers permission requests
acp/registry.tsThe ACP registry (see its FORMAT.md): agents, their distribution (npx, uvx, binary archive) and renamed ids
detect.tsAgent CLIs already on the machine, to suggest one
agents.tsInstalls any registry agent under ~/.leuria/agents/<id>@<version> (npm or Bun for npx, sha256-checked archives for binaries) and launches it
auth.tsSign-in through ACP v1: agent methods via authenticate; terminal methods (only in a TTY, with auth.terminal) by re-running the agent with the method's args; signed in means session/new succeeds
profiles.ts, codex.tsExtra hardening for agents whose defaults are unsafe. Codex gets its own CODEX_HOME, an all-features-off config, a sandbox HOME, and page tools through CODEX_CONFIG
admin.tsAdmin API for the desktop app: status, agents, sign-in, sites, site approvals, check
llm/providers.tsLLM providers: LM Studio and Ollama at their default addresses, the visitor's OpenAI-compatible APIs (~/.leuria/providers.json), model discovery (LM Studio's native API for loaded models, Ollama's for cloud models), AI ids llm:<provider>/<model>
llm/llm-session.tsThe engine's own agent loop for LLMs: streams /chat/completions with the page tools, runs tool calls through the relay, repeats until the model answers. Same surface as the ACP session
mcp-stdio.tsleuria mcp-stdio: page tools over stdio, for agents without HTTP MCP (stdio is the transport ACP requires)
policy.tsSession options that switch off the agent's own tools; permission handler that refuses anything but mcp__webmcp__*
doctor.ts, self-test.tsSetup checks, and the real round trip behind leuria test

State: ~/.leuria/config.json (port, agent), grants.json, agents/. Tests override the location with LEURIA_HOME.

SDK (packages/client, npm @leuria/client)​

FileRole
leuria.tscreateLeuria: provider cascade (select), state snapshot, connect, global middleware and events, close on unload
conversation.tsTurns: queue, turn context, timeouts, tool runner (middleware, budget, endTurn, tools the visitor answers), structured output routing, provider session switching
run.tsChatRun: an async-iterable turn with text(), object(), result()
structured.tssubmit_result tool route and JSON extraction
messages.tsMessage parts, files, context rendering, transcripts
types.tsThe contracts: Provider, ProviderSession, TurnContext, messages, events
providers/bridge.tsEngine provider: warm sessions, prompt with attachments, cancel grace
providers/browser.tsPrompt API provider
providers/server.tsOpenAI-compatible Chat Completions with a page-side tool loop
bridge/transport.tsLow-level engine client: detect, pair, sessions, SSE and tool channel with reconnects

Design rules

  • Tools always run in the page, through TurnContext.runTool, whatever the provider. That keeps events, message parts, middleware and budgets identical everywhere.
  • The history belongs to the conversation, not to the provider, so a turn can move to a better provider.
  • State is immutable snapshots plus subscribe, for useSyncExternalStore and assistant-ui's external store.
  • The page never chooses what runs on the visitor's machine.