ARCHITECTURE
The universal mobile / TV apps — architecture
Read [../VISION.md] first. This document is the *how*: how one engine becomes any device, and how the pieces fit. It is a living design doc — extend it.
One picture
┌─────────────────────────────────────────┐
per-device │ CAPABILITY PROFILE │ store-driven,
provisioning ──────▶│ "this device is X; install A, B, C" │ decided at
└───────────────────┬─────────────────────┘ provision time
│ selects
┌──────────────────────── CAPABILITIES (zero things/classes/actions) ──────────────┐
│ office storefront order-desk scanner payments assistant <anything> │
│ │ │ │
│ oracle (provider-abstracted) ─┐ └─ native primitives │
│ route per profile + policy: │ (gated 3 ways) │
│ ┌──────────────┬──────────────────┬─────┴──────────┐ │
│ │ local │ delegate: user's │ delegate: a │ │
│ │ llama.cpp │ own sub / API key│ PEER NODE on │ │
│ │ (GGUF=data) │ (ChatGPT/Claude) │ their mesh │ │
│ └──────────────┴──────────────────┴────────────────┘ │
└──────────────────────────────────┬────────────────────────────────────────────────┘
│ executes on
┌───────────────────────────────────▼────────────────────────────────────────────────┐
│ libzero — the engine, IN-PROCESS, serving 127.0.0.1, owner-trust via the door │
└───────────────────────────────────┬────────────────────────────────────────────────┘
Swift shell (iOS / tvOS) · Kotlin shell (Android / Android-TV / Fire-TV) · desktop
Two shells, every device
Shell
Language
Covers
Engine runs
mobile/android
Kotlin + NDK
Android phone/tablet, Android TV, Fire TV, Android-based handhelds/kiosks
in-process via JNI → libzero.so
mobile/ios
Swift
iPhone, iPad
in-process, link libzero.xcframework
desktop/ (exists)
ObjC / GTK / Win32
macOS, Linux, Windows
spawned child process
"Universal, all devices" = these shells. Android TV and Fire TV are the *same APK* as Android phone with a leanback flag + a tv door; the store decides the rest.
Apple TV (tvOS) caveat — honest constraint: tvOS has no WKWebView, so the webview-door model that every other shell uses cannot run there. The Apple TV path therefore needs a native SwiftUI front-end driven by the in-process engine over loopback (JSON), which is future work. Until then, the 10-foot living-room experience is carried by Android TV / Fire TV (which have WebView). The engine half (libzero in-process) is identical on tvOS — only the front-end differs.
Why mobile can't do what desktop does — and what changes
The desktop shell spawns zero as a child and points a webview at it. iOS and tvOS forbid child processes for sandboxed apps. So:
• The engine is built as a library (libzero.a / libzero.so), not only a
binary. See kernel/embed.h / kernel/embed.cpp.
• The shell calls zero_serve(cls, port, door, home) on a background thread.
It binds 127.0.0.1:<port> and blocks in the accept loop, exactly like the desktop child — only in-process. The webview loads http://127.0.0.1:<port>/.
• Dispatch is the static registry table (registry_gen.cpp), not dlsym —
no -rdynamic, no loading of downloaded code. This is the same path the Windows build uses, and it is what makes the engine notarizable on iOS.
• home is the app's private writable dir (no ambient $HOME on a phone);
zero_serve exports it as HOME so ~/zero.9.data and ~/zero.9.stores resolve under it.
Prove it on the desktop today: make embed-test && ./embed-test app 8088 mobile, then curl http://127.0.0.1:8088/home. That binary calls zero_serve exactly as the Swift / Kotlin shells will.
The door — owner trust on your own device
ZERO9_DOOR selects the door. A local door (desktop, mobile, tv) binds loopback only and is owner-trusted: holding your phone IS sitting at the machine, the same trust the terminal has. Anything else is the web door, bound wide and anonymous until authenticated. The shell sets the door (the only party that can); the DSL cannot widen it — a store refresh can never turn a private door public. See kernel/identity.cpp (isLocalDoor), functions/web/ http_listen.cpp, kernel/dispatch.cpp.
The door label also reaches the DSL as __door, so the same engine renders a 10-foot leanback view for tv and a touch view for mobile — form factor is a store decision, not a separate app.
Capability profile — "install as per the device, at that time"
A device declares *what it is* with a profile (an extension of the desktop manifest.data). It is the bridge between store-pushed behavior and shell-linked native code (Law 3 in the vision).
# capability profile (shell asset + node object) — illustrative
name = Acme Warehouse Handheld
door = mobile # or tv
start = home # first page
# DSL capabilities — pulled from the store, light up instantly
capabilities = scanner, orders, assistant
# NATIVE capabilities this device WANTS. Each lights up only if the installed
# shell was built with the backing primitive; else the app asks to update.
native = camera, barcode # e.g. a scanner handheld
# native = inference, accessibility # e.g. a flagship personal phone
# native = (none) # e.g. a TV: delegate oracle to a peer/provider
# intelligence routing for this device
oracle.provider = local | byo:openai | byo:anthropic | peer:amit-desktop
oracle.model = qwen2.5-3b-instruct-q4 # for local
A weak device and a flagship run the same app — the profile is the only difference. Provisioning sets the profile (per employee, per fleet, per use).
Intelligence: the oracle provider
oracle is one primitive with a provider behind it. The engine owns the abstraction + routing policy; it never hard-depends on a model. Targets:
• local — libzero is linked with an on-device runtime (llama.cpp; Metal on
Apple, Vulkan/OpenCL on Android). Models are GGUF *data*, managed by a model-manager capability (download / pick / evict), itself a class.
• byo:<provider> — the user's own subscription/API key, stored as a secret
on the node, called over the web. Delegation, not dependency.
• peer:<node> — oracle runs on another node of the user's mesh over iroh
(E2E). A TV with no model borrows a desktop's brain.
Policy lives in the profile, so a privacy/payment profile can pin local-only, no egress — the combination of *seeing the screen* and *calling out* is the thing to gate hardest.
Device control = actions, gated three ways
"See and manage the device" is the mobile analog of computer-use, exposed as native primitives the engine can call — but only when all three gates pass:
1. the capability is installed from the store (a class/action references it),
2. the OS permission is granted by the user (runtime prompt), and
3. a zcap grant authorizes the act.
Android native primitives (each its own functions/-style native, surfaced to the shell): AccessibilityService (read the live view tree; perform tap / swipe / text on any app), MediaProjection (screenshot), intents / deep-links, launch apps, notifications, contacts / calendar / SMS / files. iOS is far more locked down — its "control" is App Intents / Shortcuts / EventKit / share-sheet within the sandbox; true cross-app automation is Android-first. None of this is "the app can do anything" — it is actions the user installed and authorized.
Update model (two streams, like desktop)
1. Behavior (classes) — store refresh by ETag; a reload IS the latest app.
No release. This is most of the system.
2. Shell + libzero — an app-store update (or sideloaded APK). Needed only
when a *native* primitive is added/changed. The profile names native capabilities so a device can detect "my shell can't back this yet → update."
Status / roadmap
• [x] libzero in-process engine entry (zero_serve) + generalized door
(desktop / mobile / tv) + make embed-test proof on desktop.
• [ ] Android shell: NDK build of libzero.so, Kotlin Activity + WebView,
JNI bridge, profile asset, leanback variant (TV / Fire TV).
• [ ] iOS / tvOS shell: libzero.a build, Swift + WKWebView, in-process serve.
• [ ] oracle primitive + provider routing (local llama.cpp / byo / peer).
• [ ] model-manager capability (DSL) + on-device runtime link.
• [ ] device-control native primitives (Android first), three-gate permissioning.
• [ ] kiosk / LE profiles (scanner, payment), TV leanback profile.
[../VISION.md]: ../VISION.md