Add Android client-server architecture plan
ober
8d2c3becee646b3d762316ba5723aedc8202ff9a
new file mode 100644 --- /dev/null +++ b/android/PLAN.md @@ -0,0 +1,242 @@ +# Android Client-Server Architecture Plan + +*Replace embedded-binary APK with thin GUI client connecting to jcode serve in Termux over authenticated localhost.* + +## Problem + +The current APK embeds the jcode binary and runs it in Android's app sandbox. This sandbox blocks access to Termux's filesystem, binaries, and environment — meaning no git, no jerboa-mcp, no TypeScript compiler, no ssh-agent, no access to anything the user has installed in Termux. Every new tool requires rebundling the APK. + +## Solution + +Split into two components: + +- **jcode serve** (Termux): long-running server on localhost, full access to Termux environment +- **jcode APK** (Android): thin GUI client that connects over TCP, no embedded binary + +## Architecture + +``` +┌─────────────────────────────────┐ +│ Android APK │ +│ ┌───────────┐ ┌─────────────┐ │ +│ │ MainActivity│ │SettingsActivity│ +│ │ Chat UI │ │ Host/Port │ │ +│ │ Build/Plan │ │ Auth Token │ │ +│ └──────┬──────┘ └─────────────┘ │ +│ │ │ +│ JcodeClient.kt │ +│ (Socket to 127.0.0.1:PORT) │ +│ JSONL over TCP │ +└─────────┬───────────────────────┘ + │ localhost only +┌─────────┴───────────────────────┐ +│ Termux: jcode serve │ +│ │ +│ TCP listener (127.0.0.1:PORT) │ +│ Token-based auth │ +│ Same JSONL protocol as today │ +│ │ +│ Full Termux environment: │ +│ ├── git, ssh-agent │ +│ ├── scheme (Chez), jerboa-mcp │ +│ ├── node, tsc │ +│ ├── python, cargo, etc. │ +│ └── all user-installed tools │ +└─────────────────────────────────┘ +``` + +## Security + +### Threat Model + +Other apps on the same Android device can connect to localhost ports. The jcode server must reject unauthorized connections. + +### Mitigations + +1. **Bind 127.0.0.1 only** — not reachable from the network, only from apps on this device. + +2. **Pre-shared bearer token** — on first run, `jcode serve` generates a cryptographically random 256-bit token (hex-encoded, 64 chars) and writes it to `~/.jcode/server-token`. The user enters this token once in the Android app's Settings screen. + +3. **Auth handshake** — the first message from any TCP client must be `{"type":"auth","token":"<token>"}`. The server validates it against the stored token. On mismatch: log a warning, send `{"type":"error","message":"auth failed"}`, close the socket. No further communication occurs. + +4. **Single-client mode** — the server accepts one authenticated client at a time. A second connection attempt while a client is active is rejected. This prevents session hijacking if another app somehow obtains the token. + +5. **Token rotation** — `jcode serve --rotate-token` generates a new token and prints it. The old token is immediately invalidated. + +### What This Does NOT Protect Against + +- A rooted device or debuggable app that can read `~/.jcode/server-token` +- A malicious app that has already compromised Termux's uid +- These are out of scope — if Termux is compromised, the attacker already has the user's SSH keys, API keys, and full filesystem access. + +## Server Component (Scheme) + +### Changes to `src/jcode/ui/serve.ss` + +Currently `serve-main` reads/writes stdio. Add a `--port PORT` flag: + +- Without `--port`: behave as today (stdio mode, for embedded APK backward compat) +- With `--port PORT`: open a TCP listener on `127.0.0.1:PORT` + - On connection: read first message, validate auth token + - After auth: enter the same JSONL event loop as stdio mode + - On disconnect: return to listening state (accept next client) + - On SIGTERM/SIGINT: clean shutdown + +### Token Management + +- On first run with `--port`, if `~/.jcode/server-token` does not exist, generate one and print it to stderr for the user to copy. +- Read token from `~/.jcode/server-token` (one line, hex string, no newline). +- `--rotate-token` flag: overwrite file with new random token, print to stderr, exit. + +### Protocol Changes + +The JSONL protocol is unchanged except for the initial auth handshake: + +``` +Client → Server: {"type":"auth","token":"a1b2c3..."} +Server → Client: {"type":"auth_ok"} (on success) +Server → Client: {"type":"error","message":"auth failed"} (on failure, then close) + +... then normal JSONL protocol as today: +Client → Server: {"type":"user","text":"...","mode":"build"} +Server → Client: {"type":"ready","session_id":"...","session_title":"..."} +Server → Client: {"type":"token","text":"..."} +... +``` + +### Startup + +Typical Termux workflow: + +``` +$ jcode serve --port 8321 +[INFO] token: a1b2c3d4... (copy this to the Android app) +[INFO] listening on 127.0.0.1:8321 +[INFO] waiting for client... +``` + +On subsequent runs the token is reused silently (no reprint unless `--show-token`). + +## APK Component (Kotlin) + +### JcodeClient.kt (replaces JcodeProcess.kt) + +Replace `ProcessBuilder`-based subprocess management with TCP socket connection: + +- Connect to `host:port` (default `127.0.0.1:8321`, configurable in Settings) +- Send auth message with stored token +- Wait for `auth_ok` response +- Enter same JSONL read/write loop as current `JcodeProcess` reader/writer threads +- On socket disconnect: show "Disconnected — is jcode serve running in Termux?" with a Reconnect button +- Reconnect with exponential backoff (1s, 2s, 4s, max 10s) + +### SettingsActivity.kt Changes + +Add fields: +- **Server host** — text input, default `127.0.0.1` +- **Server port** — number input, default `8321` +- **Auth token** — text input (paste the 64-char hex token from Termux) +- **Connection status** — indicator (connected/disconnected/auth failed) + +Remove: +- The jcode.json / API key picker (keys are now on the server side in Termux's jcode.json) + +### MainActivity.kt Changes + +- On launch: attempt to connect to configured host:port +- Show connection state in the toolbar (green dot = connected, red = disconnected) +- If not connected: show a banner with instructions ("Start jcode serve in Termux") +- Remove all JcodeProcess subprocess logic + +### APK Build Changes (build-apk.sh) + +Remove from the build: +- jcode binary bundling (`libjcode.so`) +- `chez_sqlite_shim.so` bundling +- Termux shared lib staging (`assets/native/`, libncursesw, libiconv, libsqlite3, libz) +- The binary verification step (`file` check for ARM aarch64) +- The `JCODE_BINARY` variable and its existence check + +The APK becomes pure Kotlin — no native code, no `extractNativeLibs`. Expected size: well under 1MB. + +### Manifest Changes + +- Remove `android:extractNativeLibs="true"` (no native libs) +- Keep `android:debuggable="true"` for dev builds (remove before any real distribution) +- Keep INTERNET permission (needed for localhost socket) + +## Implementation Order + +### Phase 1: Server-side TCP listener +1. Add `--port PORT` flag parsing to `serve-main` +2. Implement TCP listener with `(std net tcp)` or raw Chez `tcp-connect`/`tcp-listen` +3. Implement token generation, storage, and validation +4. Auth handshake: validate first message, send `auth_ok` or reject +5. After auth: reuse existing `serve-loop` with TCP streams instead of stdio +6. Test from Termux: `echo '{"type":"auth","token":"..."}' | nc 127.0.0.1 8321` + +### Phase 2: APK client refactor +1. Write `JcodeClient.kt` — Socket-based JSONL with auth handshake +2. Update `SettingsActivity.kt` — host/port/token fields, remove key picker +3. Update `MainActivity.kt` — connection state UI, remove subprocess logic +4. Strip `build-apk.sh` — remove all native binary/lib bundling +5. Build, install via `adb install`, test against Phase 1 server + +### Phase 3: Polish +1. Auto-reconnect on disconnect with backoff +2. Connection status indicator in toolbar +3. "Server not running" banner with clear instructions +4. `jcode serve --background` or Termux:Boot integration for auto-start +5. Optional: mDNS/zeroconf discovery so the app finds the server without manual port config + +## MCP Integration + +With jcode serve running in Termux, jerboa-mcp (and any other MCP server) works exactly as it does on desktop: + +```json +// In Termux's ~/.jcode/config.json or jcode.json: +{ + "mcpServers": { + "jerboa": { + "command": "scheme", + "args": ["--libdirs", "/path/to/jerboa-mcp/lib", "--script", "/path/to/jerboa-mcp/main.ss"] + } + } +} +``` + +jcode serve spawns the MCP server as a stdio subprocess (existing `init-mcp-tools` code), registers its tools, and they appear in the LLM's tool list. The Android app doesn't need to know or care — it just renders tool call events like any other. + +## Git / SSH Integration + +With jcode serve running in Termux: + +- `git` is in PATH — jcode's bash tool can clone, pull, push, branch, etc. +- If the user has `ssh-agent` running and keys loaded (`ssh-add`), git SSH operations just work — jcode serve inherits `SSH_AUTH_SOCK` from the Termux environment. +- Working directory for agent sessions is any Termux-accessible path, not the sandboxed app dir. + +Typical user flow: +1. In Termux: `eval $(ssh-agent) && ssh-add ~/.ssh/id_ed25519` +2. In Termux: `cd ~/projects && jcode serve --port 8321` +3. Open jcode app: "Clone https://github.com/user/repo into ~/projects/repo" +4. jcode runs `git clone ...` via bash tool — works because git and SSH are available. + +## File Sizes (estimated) + +| Component | Before | After | +|-----------|--------|-------| +| APK | 16 MB | < 1 MB | +| Termux server | (embedded) | 0 (already installed as `jcode` binary) | +| Total on device | 16 MB | < 1 MB + existing jcode | + +## Open Questions + +1. **Multiple clients** — should the server support multiple simultaneous APK connections (e.g., tablet + phone)? Current plan says no (single-client). Could revisit. + +2. **Session persistence** — sessions are stored in Termux's SQLite DB. The app is stateless. Is this fine, or should the app cache recent messages for offline viewing? + +3. **Notifications** — should the APK show a notification when a long-running agent turn completes while the app is backgrounded? Requires a foreground service. + +4. **Auto-start** — should we provide a Termux:Boot script to auto-start `jcode serve` on device boot? + +5. **TLS** — localhost doesn't need encryption (no network exposure), but adding TLS with a self-signed cert would prevent other local apps from even seeing the traffic via packet capture. Low priority.