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.
The layers
Section titled “The layers” 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: theflutter_rust_bridgeglue overtealhand-core, plus iroh’s Android JNI hook (called once, fromApplication.onCreate, and guarded by aOnce). It is the only crate on the phone allowedunsafe, and it has no logic of its own. It has no call that takes anOrigin: 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, andprepare_from_agentfor a computer’s AI apps), signing requests and sending (confirm), one livectl/1link 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.
Computer
Section titled “Computer”tealhand-files: the shared folders (shares.bin), the path jail (the only code that turns a wire path into an OS path), thefiles/1server, the downloader with its address rule, and the phone’s file client. It never depends ontealhand-host: the Host plugs in through theFilesGatetrait.tealhand-host(library andtealhand-hostbinary): opens the mesh, servesctl/1,files/1andpair/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 callstealhand-powerwith 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 (thesysinfocrate) 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 runis the same Host without a UI. - A computer’s own client (decision 52):
tealhand-corein computer mode, opened byHostHandle::open_peersafter 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 thePeerstrait. 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-hostmcp/): while the switch is on, the Host serves MCP’s Streamable HTTP on127.0.0.1with a bearer token from the OS credential store;tealhand-host mcpis the stdio shim AI apps launch. Each tool call becomes anActionchecked withcheck_origin(Origin::Agent)(reads only) and goes out through the computer’s own client, whereprepare_from_agentchecks it again.
Servers
Section titled “Servers”tealhand-relay: aniroh-relayserver 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.
How a command flows through the code
Section titled “How a command flows through the code”tealhand-core::prepare_from_chatorprepare_from_buttonvalidates 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 withAction::card.tealhand-core::confirmbuilds aCommandBody(60-second lifetime), asks the app for thedevicesignature (andpresencewhenneeds_presence), and sendsRequeston thectl/1stream.tealhand-mesh’s roster gate already admitted the connection.tealhand-hostdecodes theSignedCommandand runstealhand-identity::Verifier::verify.- 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 aResponsegoes back; a change by a computer is also told to the Host’s phones.
Design rules the code follows
Section titled “Design rules the code follows”- 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-powercan sleep or shut down; no shell anywhere.