Skip to content

Architecture

Tealhand is a Rust workspace (crates/*) plus two apps: a Flutter phone app (apps/mobile) and a Tauri 2 app for computers (apps/desktop, built from source, no installer yet). The core is Rust on iroh 1.x. For a map with every link, see The big picture.

tealhand-actions the one action list (no other tealhand crate)
tealhand-proto envelope, frames, ALPNs, error codes (no other tealhand crate)
|
tealhand-identity keys, signed roster, signed commands, verifier, connecting logic
| (uses actions and proto; no networking)
tealhand-mesh iroh endpoint, roster gate, connection door, rate limits
|
tealhand-files shared folders, path jail, files/1 server and client
| (the Host serves it, the phone's core is its client)
+-----------------+------------------+
| |
tealhand-core (phone) tealhand-host (computer) <-- tealhand-sysinfo
| | (CPU, GPU, tasks)
tealhand-ffi (bridge) tealhand-power (process: sleep, shut down, check-users)
| |
apps/mobile (Flutter) apps/desktop (Tauri 2, embeds tealhand-host)
tealhand-relay = iroh-relay server + tealhand-identity (verifies rosters, never signs)
tealhand-planner = axum HTTP service + tealhand-actions (schema, validation) + Ollama on 127.0.0.1
  • Flutter app (apps/mobile): screens, the ask box, the cards. It owns the two P-256 keys in the Android Keystore and signs in Kotlin. The screens are built: Home, Devices, Power, Files (with Downloads), the share sheet, the terminal with saved commands (SSH access), and Settings.
  • tealhand-ffi: the flutter_rust_bridge glue over tealhand-core, plus iroh’s Android JNI hook (called once, from Application.onCreate, and guarded by a Once). It is the only crate on the phone allowed unsafe, and it has no logic of its own. It has no call that takes an Origin: the app’s chat path and its buttons are separate entry points in the core.
  • tealhand-core: connecting, cards (prepare_from_chat, prepare_from_button, and prepare_from_agent for a computer’s AI apps), signing requests and sending (confirm), one live ctl/1 link per connected computer with reconnects, the roster duties (push to each computer, upload to the relay), the planner client and action search. When it needs a signature it emits a sign request; the app answers with the signature, which the core checks against the public key before use.
  • tealhand-files: the shared folders (shares.bin), the path jail (the only code that turns a wire path into an OS path), the files/1 server, the downloader with its address rule, and the phone’s file client. It never depends on tealhand-host: the Host plugs in through the FilesGate trait.
  • tealhand-host (library and tealhand-host binary): opens the mesh, serves ctl/1, files/1 and pair/1, and runs everything through one place where a request is checked (parse, validate, origin, presence, remote control). It runs the rules engine every 5 seconds, the 60-second notice, and the activity journal. It calls tealhand-power with one fixed argument and no shell, and refuses a shutdown or sleep rule at once when the helper cannot be used.
  • tealhand-sysinfo: reads CPU, memory, battery, uptime and processes (the sysinfo crate) and NVIDIA GPUs (NVML, loaded only if the driver is there). Unknown values are “none”, never 0. It only reads.
  • Tealhand on a computer (apps/desktop, Tauri 2): embeds the Host library in its own process, so there is no local network port or pipe between the UI and the Host. tealhand-host run is the same Host without a UI.
  • A computer’s own client (decision 52): tealhand-core in computer mode, opened by HostHandle::open_peers after each Host start. It signs in process with the computer’s iroh key and dials through the Host’s own endpoint (two iroh endpoints with one key must not run in one process). The Tealhand app’s other-computer pages call it through the Peers trait. A Host takes a computer’s command like a phone’s, without the presence rule, and reports each change to its phones (PeerChanged).
  • AI apps (decision 53, tealhand-host mcp/): while the switch is on, the Host serves MCP’s Streamable HTTP on 127.0.0.1 with a bearer token from the OS credential store; tealhand-host mcp is the stdio shim AI apps launch. Each tool call becomes an Action checked with check_origin(Origin::Agent) (reads only) and goes out through the computer’s own client, where prepare_from_agent checks it again.
  • tealhand-relay: an iroh-relay server whose access control admits only endpoints listed in a stored, unexpired roster of an allowed owner, plus a separate HTTPS listener for roster uploads. AGPL-3.0-only.
  • tealhand-planner: POST /v1/plan. Builds the model’s JSON Schema from the action list, asks a local Ollama, and checks the answer with the same parser the phone uses. No storage, no key.
  1. tealhand-core::prepare_from_chat or prepare_from_button validates the action, sets its origin (chat, or the app’s own buttons: Dart never chooses it), checks that a phone may send it, and builds the card with Action::card.
  2. tealhand-core::confirm builds a CommandBody (60-second lifetime), asks the app for the device signature (and presence when needs_presence), and sends Request on the ctl/1 stream.
  3. tealhand-mesh’s roster gate already admitted the connection. tealhand-host decodes the SignedCommand and runs tealhand-identity::Verifier::verify.
  4. Only then Action::decode, validate, check_origin(Remote), the presence rule (waived for a computer issuer, except for a saved command the owner marked for the fingerprint) and the remote control rule. Then the action runs and a Response goes back; a change by a computer is also told to the Host’s phones.
  • Verify, then parse. A signed blob is checked before its inner bytes are decoded.
  • One encoding. postcard everywhere on the wire and on disk; JSON only for the planner and the schema. Strict decoding: no bytes left over, and signed structures must re-encode to the same bytes.
  • Wire enums are append-only. Their variant order is the wire format.
  • Domain separation. Every signed or hashed message starts with its own string: tealhand-cmd-v1, tealhand-roster-v1, tealhand-pair-v1, tealhand-bind-v1.
  • No content in logs. No sentences, file contents, keys or process names; addresses never in error texts.
  • Least privilege. The Host runs as the signed-in user; only tealhand-power can sleep or shut down; no shell anywhere.