diff --git a/AGENTS.md b/AGENTS.md index cbac4a1..90182fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,31 +30,58 @@ can't come apart). Read local-updater's `README.md` and `AGENTS.md` for the conventions before diverging from them; module-by-module intent for this repo is in PLAN.md's "Backend layout" section. -- `server/` — Rust backend (to be created, phase 1). -- `app/` — Compose Android app (to be created, phase 1). +- `server/` — Rust backend (`ai-server`). `main.rs` bootstraps (TLS, the + auth layer, token/QR enrollment, wg0 binding), `routes.rs` has the HTTP + table in its module doc comment, `auth.rs` the bearer-token middleware, + `config.rs` the persisted schema, `session/` the manager (registry + pattern), `Driver` trait + event model, `EchoDriver`, and transcripts. +- `app/` — Compose Android app, single `:androidApp` module, package + `com.example.aiapp`, label "AI Sessions". `AppRoot.kt` is the navigation + `when`; `Api.kt`/`EventStream.kt` the REST + SSE clients; `Events.kt` the + event model mirror; `ServerConfig.kt` settings + Keystore-sealed token; + screens in `SessionListScreen/SessionScreen/SpawnScreen/SettingsScreen`. - `gen-dev-cert.sh` / `certs/` — copied from local-updater's scheme (idempotent CA, reissued leaf; regenerating the CA strands the installed - app — same one-way door). + app — same one-way door). Dev SANs cover 127.0.0.1, 10.0.2.2 (emulator → + host), and the LAN IP alongside the WireGuard address. ## Status -Pre-implementation. Phases are in PLAN.md; phase 1 (Skeleton) proves the -whole pipe — TLS, token auth, wg0-bound listener, SSE with transcript -cursors, both app screens — against a fake `EchoDriver` before any AI is -involved. Every phase ends runnable and verified against the real thing. +Phase 1 (Skeleton) done 2026-08-24 — the whole pipe works end-to-end +against `EchoDriver`: TLS + token auth, SSE with transcript cursors +surviving restarts, spawn/message/question/delete from the app. Next: +phase 2 (Claude driver); phases are in PLAN.md. ## Checking your work -Fill in real commands as they're created; until then, the inherited posture: - -- Server: `cargo test` + `cargo clippy --all-targets` from `server/` — - the build stays warning-clean from the first commit. -- App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin`. -- Tests where logic is pure (event normalization, transcript cursors, config - persistence, llama-server refcounting); the app is UI over the API and is - verified by running it. +- Server: `./run-tests.sh` (or `cargo test`) + `cargo clippy --all-targets` + from `server/` — the build stays warning-clean, keep it that way. +- App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin`; + `./run-android.sh` builds, installs, and launches on the emulator. +- Run the server for development with `--bind 127.0.0.1` (wg0 doesn't exist + on this machine yet; the default fails closed). First run prints the + enrollment QR/URI with the token — capture it from the log. - Prefer exercising the server directly over going through the UI: - `curl --cacert certs/ca.pem -H "Authorization: Bearer …" https://…`. + `curl --cacert certs/ca.pem -H "Authorization: Bearer …" https://127.0.0.1:8443/sessions`. + The emulator app reaches it at `https://10.0.2.2:8443`; enroll it with + `adb shell "am start -a android.intent.action.VIEW -d 'aiapp://enroll?host=10.0.2.2&port=8443&token=…'"` + (quote so the device shell doesn't eat the `&`s). + +## Things that have bitten + +- **tracing caches callsite interest process-wide.** A test that hits a + `tracing::warn!` with no subscriber installed can poison the interest + cache for a concurrent test that captures logs (flaky "nothing was + logged" failures). Keep every exercise of a logging code path under the + one capturing subscriber — that's why the auth middleware has a single + combined gating+logging test. +- **The keyboard pans the window unless the activity opts into resize.** + Without `android:windowSoftInputMode="adjustResize"`, opening the IME + slides the whole window up (top bar off screen) instead of resizing — + `imePadding()` alone doesn't fix it and the transcript looks empty. +- **CMP 1.11 deprecates the `compose.*` dependency accessors** — declare + `org.jetbrains.compose.:` directly (material3 has its own release + train, separate from the CMP version). ## Environment notes (this machine, learned in local-updater) diff --git a/PLAN.md b/PLAN.md index 0ec0c7a..083dc14 100644 --- a/PLAN.md +++ b/PLAN.md @@ -83,6 +83,10 @@ clap, tracing. Rust edition 2024, warning-clean, clippy in CI habit. Driver output, whatever the dialect, is normalized into one event enum before it touches the transcript or the phone: +- `UserMessage { text }` — what the user sent, echoed into the transcript + by the manager (not by drivers) so every device renders the conversation + from the one stream. (Added 2026-08-24 during phase 1: without it, + reconnects and second devices would lose the user's side.) - `AssistantText { delta }` — streaming text (rendered as markdown). - `ToolStart / ToolUpdate / ToolEnd { tool, input, output }` — the "view tools it's running" screen is just these. @@ -91,6 +95,10 @@ it touches the transcript or the phone: - `Question { id, prompt, options }` — anything the session needs a human for: Claude's AskUserQuestion, and **permission requests** (canUseTool) are the same shape with approve/deny options. Answered via one endpoint. +- `Answered { id, answer }` — the manager's record of a question being + answered, so a rendered question card resolves on every connected device, + not just the one that answered (added 2026-08-24, same reasoning as + `UserMessage`). - `Status { state }` — idle / running / awaiting-input / compacting / exited. - `UsageDelta { tokens }` — per-turn token counts where the dialect reports them (both do). @@ -244,11 +252,17 @@ everything spawning one created. ANSI), encoding `aiapp://enroll?host=…&port=…&token=…`. The CA stays embedded in the APK (`PinnedCert.kt` pattern), so the QR carries no trust material — photographing the terminal leaks only the token - (rotatable), never a way to weaken pinning. + (rotatable), never a way to weaken pinning. The app side needs no QR + library at all: it registers an intent filter for the `aiapp://enroll` + scheme, and the stock camera app hands the scanned URI straight to + `MainActivity` (2026-08-24). - **Storage**: server keeps only the SHA-256 in `config.json` (plain hash is enough for high-entropy random input; buys that a leaked config doesn't leak the credential). No "show token again" — lost means rotate. - Phone side: Keystore-backed encrypted preferences. + Phone side: sealed with an Android Keystore AES-GCM key (a small + hand-rolled helper in `ServerConfig.kt` — Jetpack's + EncryptedSharedPreferences is deprecated with no drop-in successor, and + Google's guidance is now "use Keystore directly"; 2026-08-24). - **Transport**: `Authorization: Bearer` header on every request including the SSE GET. Never a query parameter (URLs leak into logs). The tracing layer must not log the header — covered by a test so a logging change @@ -306,7 +320,10 @@ everything spawning one created. exists even inside the tunnel, so the token can't travel unencrypted by misconfiguration, and interface binding failing closed (refuse to start if `wg0` is absent, rather than falling back to 0.0.0.0) is part of the - same guarantee. + same guarantee. Development gets `--bind ` as an *explicit, logged* + override (loopback for curl, a LAN address for a pre-WireGuard phone) — + a deliberate flag, never a fallback, so the fail-closed default is + untouched (2026-08-24). - The bootstrap-over-HTTP trick from the updater is unnecessary here — the app installs via Local Updater. @@ -368,11 +385,16 @@ window just fills. ## Phases -1. **Skeleton** — repo layout, cert script, TLS + token auth, wg0-bound - listener (fail closed if the interface is missing), config.json, - session registry with a fake `EchoDriver`, session list + session screen in - the app end-to-end over SSE. Proves the whole pipe before any AI is - involved. +1. **Skeleton** — *done 2026-08-24.* Repo layout, cert script, TLS + token + auth, wg0-bound listener (fail closed if the interface is missing), + config.json, session registry with a fake `EchoDriver`, session list + + session screen in the app end-to-end over SSE. Proves the whole pipe + before any AI is involved. Verified: 10 server tests + clippy clean; + curl end-to-end over pinned TLS (auth rejection, spawn, SSE + replay/resume by cursor, question round trip, restart continuing seq + numbers, delete); the app on the `tdep` emulator against the real + server (QR-style enrollment via deep link, spawn, streamed echo turn, + question answer, tool card). 2. **Claude local** — ClaudeDriver: spawn, stream text/tools, mid-run send, interrupt, permission questions, AskUserQuestion, images both ways, delete. *Milestone: daily-drivable Claude replacement on localhost.*