Skip to content

Android: run coding agents on the phone (stacked on #609) - #639

Draft
katulevskiy wants to merge 1 commit into
zeronsh:zeron/android-app-implementationfrom
katulevskiy:android-on-device
Draft

katulevskiy wants to merge 1 commit into
zeronsh:zeron/android-app-implementationfrom
katulevskiy:android-on-device

Conversation

@katulevskiy

@katulevskiy katulevskiy commented Sep 29, 2026 •

Copy link
Copy Markdown

Stacked on #609. This PR targets zeron/android-app-implementation, so the diff shows only what it adds to #609's app. Once #609 merges, GitHub moves this PR's base to main.

Summary

Adds on-device mode to #609's Android app: the phone runs its own zeron headless engine and the real agent CLIs (Claude Code, Codex, OpenCode, Grok, …) inside a small Linux guest on the device. #609's Account (remote control) and Demo modes are unchanged, and on-device mode sits next to them.

Tested on a real arm64 phone: Claude Code (Max) and Codex (ChatGPT) signed in and ran sessions entirely on the device, including a Claude session with image attachments.

On a real phone (runtime and engine from this PR, shown in an earlier standalone UI):

Sessions Codex on-device Claude Code with images Agents signed in Settings

In #609's UI (Android 16 emulator):

First run On-device engine Coding agents Session on the phone

How it works

Compose UI (#609) ── zeron-mobile (UniFFI) ── zeron-client (Live backend, Credentials::Local)
                                                 │ ws/http 127.0.0.1:27655 + bearer
RuntimeService (foreground) ── libproot.so ─▶ Alpine guest
                                               └─ zeron headless (static musl)
                                                    ├─ local edge   :27655
                                                    ├─ engine IPC   :27654 (token-gated)
                                                    └─ claude / codex / opencode / git / node …
  • Why a Linux guest. Android forbids execve of app-writable files (targetSdk ≥ 29), and every vendor installer writes executables. proot's loader maps guest programs into memory instead of exec'ing them. The app ships proot and the engine as lib*.so in nativeLibraryDir, the only place apps may exec from. The engine runs inside the guest, so it believes it is on a Linux VPS: zero engine changes for spawning, installing, or discovering harnesses. UserLAnd uses the same approach on Play.
  • Why a local edge. zeron-client only speaks the edge protocols. crates/localedge is a single-tenant Rust port of edge/src (chat2 rooms, registry room, DeviceRoom relay, nudges, checkpoints; SQLite-backed), hosted by zeron headless on loopback. The unmodified engine and client sync over production protocols with no cloud. It's also a first step toward the self-hosting contract ARCHITECTURE.md defers.
  • Loopback is shared by every app on a phone, so both listeners require a token. The edge takes a bearer (header or ?token=, compared in constant time). The IPC socket is gated by ZERON_IPC_TOKEN, which is passed explicitly to the zeron mcp server that agents get.

What's in the PR (on top of #609)

Rust

  • crates/localedge (new): the local edge, with unit, edge-protocol, and e2e tests. The e2e test drives a real in-process engine (mock harness) ⇄ local edge ⇄ zeron-client, and covers restart persistence and auth rejection.
  • apps/zeron:
    • a default ui feature, so --no-default-features builds an engine-only static musl binary
    • ZERON_LOCAL_EDGE_PORT/TOKEN embedding
    • zeron status reports the local edge
  • crates/rpc: ZERON_IPC_TOKEN gate on the server; the client presents it, and a rejected handshake gets a clear error.
  • crates/engine: ZERON_DEVICE_PLATFORM; the token is injected into the agent MCP server.
  • crates/proto / crates/client:
    • execution hosts are recognised by advertised capabilities, not platform names, so a phone engine can host sessions while iOS behaviour is unchanged
    • Credentials::Local
    • relayed InstallHarness / CloneRepo / FetchAll get the engine's 15-minute forward deadline instead of 30 s
  • crates/mobile: a host_call passthrough; a demo host for installs and clones; clippy fixes.

Android

  • :runtime (new module):
    • bootstrap: extract the Alpine rootfs, create the guest user, set DNS, install packages
    • a foreground service with restart/backoff and clean stop
    • health checks, log rotation, exec()
    • phantom-process-kill detection
    • in-place guest upgrades on app update
    • agent environment hints (~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, …) and a sudo shim; apk add works unprivileged in the guest
  • :app, in Android app on the shared Rust mobile core (Material 3 Expressive) #609's design (Material 3 Expressive, Zeron icons):
    • "Run agents on this phone" on the first-run screen, and a mode switch in Settings
    • On-device engine screen: status, start/stop/reset, battery and notification permissions, log
    • Coding agents screen, for any engine device including remote hosts in Account mode: install with progress and cancel, sign in (browser or pasted code), add account, sign out
    • new sessions default to an installed harness; clone repositories on the phone; local notifications
  • Build:
    • useLegacyPackaging = true, so the runtime executables are extracted to nativeLibraryDir
    • the proot and engine libraries are kept unstripped (stripping corrupted the patched libtalloc)
    • Gradle runs fetch-proot.sh, fetch-rootfs.sh and build-engine.sh when their outputs are missing
  • scripts/android: fetch and patch proot (the x86_64 build is rebuilt with a fork→clone patch, because Android's x86_64 seccomp policy rejects musl's fork), fetch Alpine, build the musl engine.

Full design and contracts: docs/android.md.

Testing

  • Tests:
    • cargo check --workspace --all-targets
    • cargo test for localedge (16), client (43), rpc (30), proto (47), mobile (20)
    • Android JVM unit tests
  • Emulator (Android 16, x86_64), this branch:
    • clean install → Run agents on this phone → guest set up → OpenCode installed from Coding agents → clone into /home/zeron/projects
    • OpenCode Zen sessions wrote and ran hello.py / fib.py in the guest
    • stop, reset, and relaunch with autostart
    • Demo mode and the Account sign-in screen still work
  • Real phone (arm64): Claude Code and Codex signed in and ran sessions on the device, using the same runtime and engine in the earlier standalone UI (screenshots above).

Known gaps

  • Not yet verified:
    • this combined UI on real arm64 hardware
    • WorkOS sign-in and agent browser sign-in inside this UI (the emulator's system_server crashes whenever a Custom Tab opens; see docs/android.md)
    • background notifications
    • a real phantom-process kill
  • No release build: per-ABI splits and Play's 16 KB alignment for libproot-loader32.so are still open.
  • Separate modes: the phone's engine can't yet join an account as a device next to your desktops; on-device and Account are separate modes.
  • Follow-up design (not in this PR): capsules, i.e. moving, forking, or offloading a running session between devices, or out to a rented VM.

Try it

cd apps/android && ./gradlew :app:assembleDebug   # builds the core, fetches proot/Alpine, builds the musl engine

Toolchain: rustup targets {aarch64,x86_64}-linux-android and {aarch64,x86_64}-unknown-linux-musl, cargo-ndk, cargo-zigbuild + zig, Android SDK 37 + NDK 29, JDK 21.

@katulevskiy

Copy link
Copy Markdown
Author

HEADS UP, THIS IS HEAVILY VIBECODED

Adds on-device mode to the Material 3 Expressive app: the phone runs its own
`zeron headless` plus the real agent CLIs (Claude Code, Codex, OpenCode,
Grok…) in a proot Alpine guest, next to zeronsh#609's Account and Demo modes.

Engine side
- crates/localedge: single-tenant Rust port of the edge (chat2 rooms,
  registry room, DeviceRoom relay, nudges, checkpoints) on loopback, so the
  unmodified engine and zeron-client sync over production protocols with no
  cloud. `zeron headless` hosts it via ZERON_LOCAL_EDGE_PORT/TOKEN.
- ZERON_IPC_TOKEN gates the IPC socket (loopback is shared by every app on a
  phone); ZERON_DEVICE_PLATFORM; a `ui` cargo feature for an engine-only
  static musl build.
- Client: Credentials::Local; execution hosts recognised by advertised
  capabilities rather than platform; long host RPCs (installs, clones) get
  the engine's forward deadlines instead of 30s. Mobile: host_call.

Android
- :runtime module: bootstrap, foreground RuntimeService, health, logs,
  exec(), phantom-kill detection, in-place guest upgrades.
- App: "Run agents on this phone" on the first-run screen and a mode switch
  in Settings; On-device engine and Coding agents screens (install, sign in,
  sign out, for any engine device); new sessions default to an installed
  harness; clone repositories on the phone; local notifications.
- scripts/android: fetch/patch proot (x86_64 rebuilt with a fork→clone
  patch), fetch Alpine, build the musl engine — run by Gradle when missing.

Design and contracts: docs/android.md.
@katulevskiy katulevskiy changed the title Android: run coding agents on the phone Android: run coding agents on the phone (stacked on #609) Sep 29, 2026
@katulevskiy
katulevskiy changed the base branch from main to zeron/android-app-implementation September 29, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant