Architecture
The six components of GemmaPod, how the bytes flow between them, and where the trust boundaries are.
The component map
GemmaPod is six things working together. Five of them are layers of software; the sixth is the agent itself.
POD one signed .html capsule = one agent
├─ TOOLKIT build & sign @gemmapod/toolkit
├─ HOST device runtime @gemmapod/host
│ ├─ MODEL local LLM bridge (Ollama or OpenAI-compatible)
│ └─ BUS pod registry, event stream, conversation store
├─ SIGNAL WebRTC relay @gemmapod/signal
├─ EMBED browser runtime @gemmapod/embed (+ @gemmapod/shim, @gemmapod/core)
└─ gemmapod unified CLI npm i -g gemmapodA single sentence to hold the whole thing:
Pods are the agents. The Toolkit builds them. Your Host runs them. The Model thinks for them. Signal lets them reach visitors. Embed lets a webpage hold one.
Glossary
Pod
A single signed .html file containing one agent. The capsule bundles:
- A signed manifest (Ed25519 over CBOR) describing identity, persona, signed tool allow-list, and transport hints.
- A WASM core (Rust) that verifies the manifest at boot and signs DARTC envelopes at runtime.
- A Preact widget that hosts the chat UI and falls back to a small in-browser model when the Host is unreachable.
You make one with gemmapod create or by
hand-writing pod.toml and running gemmapod build.
Toolkit — @gemmapod/toolkit
The build and signing library. Reads pod.toml, signs the manifest
with the owner's Ed25519 key, inlines the WASM core and the embed
runtime, and writes a self-mounting .html.
Also produces fresh owner keypairs (gemmapod keygen) and validates
manifests against the schema (gemmapod doctor <pod.toml>).
The unified CLI uses Toolkit as a library; you don't import it directly unless you're scripting a build pipeline.
Host — @gemmapod/host
The local runtime that runs on your device. One Host process can run many Pods. It owns:
- The pod registry — which pods are loaded, their signal state, resume on restart.
- The event bus — every pod-scoped and host-scoped event flows
here, available to dashboards and
gemmapod logs. - The conversation store — SQLite at
~/.gemmapod/host.sqlite, keyed by(podId, conversationId). - The HTTP API at
http://127.0.0.1:<port>/api/v1/*— used by the CLI and the bundled dashboard. Loopback-only, token-gated.
Start with gemmapod start (or implicitly via the first gemmapod run).
Stops with gemmapod stop or Ctrl-C.
Model
Not a package — a role. The LLM the Host proxies pod requests to.
Default is local Ollama (http://localhost:11434) running
gemma4:e4b. Override per pod via pod.toml or per run via
gemmapod run --model <name>. Any OpenAI-compatible endpoint will work
in place of Ollama.
Signal — @gemmapod/signal
The signaling broker that lets a pod in a visitor's browser find your Host. It carries:
- WebRTC SDP rendezvous — short-lived offer/answer exchange. Once the data channel is open the broker is out of the loop.
- Pod registry —
POST /podsuploads the signed capsule;GET /:idserves it. Pluggable storage; memory + SQLite ship.
It never sees chat plaintext. The reference broker runs at
wss://signal.gemmapod.com/signal; self-host with
@gemmapod/signal if you want your own.
Embed — @gemmapod/embed
The browser-side runtime that brings a pod to life inside a web page,
email, or any HTML host. Republishes the
two @gemmapod/shim
IIFEs (full UI + headless) under stable CDN paths, with bundled .d.ts
for module consumers. The shipped pod .html carries its own copy of
the runtime inlined — Embed is what you reach for when you want to
load a pod from a <script> tag without using the capsule format.
gemmapod — the unified CLI
gemmapod create # interactive wizard → pod.toml + owner.key
gemmapod build # sign & bundle into agent.html
gemmapod run # add a pod to your Host (starts one if none running)
gemmapod start # boot the Host with no pods
gemmapod list # see running pods
gemmapod logs # tail events for one pod
gemmapod status # probe Host + Ollama + Signal broker
gemmapod stop # stop a pod or shut the Host downFull reference: /docs/reference/cli.
Bytes-flow walkthrough
The six nouns describe the shape. Below is what actually happens when a visitor opens a pod.
1. Pod build
gemmapod build pod.toml --key owner.key --out my-pod.htmlToolkit reads pod.toml, runs GemmaPodCore.signManifest(manifest, key)
via WASM (Node target), inlines the CBOR + signature + the prebuilt
shim IIFE + the WASM bytes (as a data: URL) into a 960 KB HTML
template, and writes the artefact. No network. Toolkit also
round-trip-verifies before emitting; a bad signature is a CLI error,
not a runtime surprise.
2. Pod boot
The visitor opens the .html. The inlined <script> calls
GemmaPod.boot(el):
- Init WASM core from inlined bytes (~0 ms; no fetch).
GemmaPodCore.verifyManifest(b64ToBytes(manifestB64)). Fails closed — a tampered manifest shows the user a red refusal box.- Translate the verified manifest into a
PodConfig(browser-shape). mountPod(el, config, { fallbackUi: "default" })— constructs the runtime, mounts the Preact widget, attaches the WebGPU prepare panel if a fallback transport is configured.
3. Transport selection
selectTransport(config):
- WebRTC first. Open a WebSocket to
transport.dartc.signalUrl. Wait for the data channel labelleddartc.v0to open. If the broker reports the Host as offline, drop through. - Fallback. Construct (but do not prepare) a
FallbackTransport. The host UI shows a panel; nothing downloads until the user clicks. - Direct. A development convenience — straight HTTP to a local Ollama. Only useful when visitor and Ollama are on the same LAN.
4. DARTC handshake (WebRTC path)
Once the data channel opens, both peers exchange signed dartc.hello
envelopes. The visitor side generates an ephemeral Ed25519 session
key; the Host side signs with its own session key. Both sign a copy of
the signed pod manifest into the hello payload so the Host can verify
identity and the visitor knows it's talking to the right owner.
The Host also sends an A2A-shaped AgentCard on a2a.discovery,
exposing the signed tools as A2A skills.
5. Chat
Visitor sends gemmapod.chat.request over the channel. Host streams
back gemmapod.chat.delta frames and, interleaved, signed
gemmapod.ui.event envelopes — run lifecycle, tool calls, state
snapshots, custom events. The runtime auto-applies STATE_* events
into runtime.state, MESSAGES_SNAPSHOT into runtime.chat, and
re-emits everything as ui.event on the bus.
6. Conversation memory
The browser keeps a stable conversationId per (pod, host) pair in
localStorage. A page refresh creates a fresh WebRTC peer and a new
ephemeral DARTC session key, but the runtime sends the same
conversationId in the next dartc.hello so the Host can reattach to
the existing thread.
The Host stores conversation memory in a local SQLite at
~/.gemmapod/host.sqlite keyed by (podId, conversationId). The
WebRTC peer is short-lived; the conversation is durable.
Trust boundaries
| Boundary | Trust assumption |
|---|---|
| Manifest signature | Visitors trust the owner pubkey, the way you trust an email sender. |
| WASM core | Same Rust source on every side. pkg/ (web) and pkg-node/ (node) cannot drift. |
| DARTC frame signing | Ephemeral session keys; replay window enforced by msg_id. |
| Tool execution | Host enforces the manifest's signed allow-list. Pods cannot escalate. |
| Signal broker | Carries SDP + signed blobs only. Never sees chat plaintext. |
| In-browser fallback | Visitor's machine runs the model entirely; no network during chat. |
Detailed threat model: Security model.
Package map
| Component | Package(s) | Role |
|---|---|---|
| Pod | (the capsule itself — not a package) | One signed .html = one agent. |
| Toolkit | @gemmapod/toolkit | Build, sign, validate. Used by the CLI; usable as a library. |
| Host | @gemmapod/host | Local runtime: registry, bus, conversation store, HTTP API, dashboard. |
| Model | (external — Ollama or OpenAI-compatible) | The LLM the Host proxies to. Not a @gemmapod/* package. |
| Signal | @gemmapod/signal | WebRTC handshake relay + pod registry. Self-host if you want your own. |
| Embed | @gemmapod/embed, @gemmapod/shim, @gemmapod/core | Browser runtime. embed is the public CDN/npm face; shim is the source; core is the Rust/WASM signer/verifier. |
| CLI | gemmapod | The developer front door. Calls into Toolkit and Host. |
| Protocol | @gemmapod/dartc | DARTC envelope types, canonical JSON, topic helpers, UI-event types. No deps. |