Record phase 1 completion and the decisions it added

PLAN.md: UserMessage/Answered events in the common model, the --bind dev
override (fail-closed default untouched), enrollment via the aiapp://
intent filter, Keystore-sealed token storage; phase 1 marked done with
what was verified. AGENTS.md: real layout, commands, and the lessons
that bit (tracing callsite cache in tests, adjustResize, CMP accessor
deprecation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-24 21:08:36 -04:00
1 parent 213bc72b64
commit f3cebeea78
2 files changed
+73 -24

No files matched your search

+43 -16
View File
@@ -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 conventions before diverging from them; module-by-module intent for this
repo is in PLAN.md's "Backend layout" section. repo is in PLAN.md's "Backend layout" section.
- `server/` — Rust backend (to be created, phase 1). - `server/` — Rust backend (`ai-server`). `main.rs` bootstraps (TLS, the
- `app/` — Compose Android app (to be created, phase 1). 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 - `gen-dev-cert.sh` / `certs/` — copied from local-updater's scheme
(idempotent CA, reissued leaf; regenerating the CA strands the installed (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 ## Status
Pre-implementation. Phases are in PLAN.md; phase 1 (Skeleton) proves the Phase 1 (Skeleton) done 2026-08-24 — the whole pipe works end-to-end
whole pipe — TLS, token auth, wg0-bound listener, SSE with transcript against `EchoDriver`: TLS + token auth, SSE with transcript cursors
cursors, both app screens — against a fake `EchoDriver` before any AI is surviving restarts, spawn/message/question/delete from the app. Next:
involved. Every phase ends runnable and verified against the real thing. phase 2 (Claude driver); phases are in PLAN.md.
## Checking your work ## Checking your work
Fill in real commands as they're created; until then, the inherited posture: - Server: `./run-tests.sh` (or `cargo test`) + `cargo clippy --all-targets`
from `server/` — the build stays warning-clean, keep it that way.
- Server: `cargo test` + `cargo clippy --all-targets` from `server/` - App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin`;
the build stays warning-clean from the first commit. `./run-android.sh` builds, installs, and launches on the emulator.
- App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin`. - Run the server for development with `--bind 127.0.0.1` (wg0 doesn't exist
- Tests where logic is pure (event normalization, transcript cursors, config on this machine yet; the default fails closed). First run prints the
persistence, llama-server refcounting); the app is UI over the API and is enrollment QR/URI with the token — capture it from the log.
verified by running it.
- Prefer exercising the server directly over going through the UI: - 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.<x>:<x>` directly (material3 has its own release
train, separate from the CMP version).
## Environment notes (this machine, learned in local-updater) ## Environment notes (this machine, learned in local-updater)
+30 -8
View File
@@ -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 Driver output, whatever the dialect, is normalized into one event enum before
it touches the transcript or the phone: 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). - `AssistantText { delta }` — streaming text (rendered as markdown).
- `ToolStart / ToolUpdate / ToolEnd { tool, input, output }` — the "view tools - `ToolStart / ToolUpdate / ToolEnd { tool, input, output }` — the "view tools
it's running" screen is just these. 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: - `Question { id, prompt, options }` — anything the session needs a human for:
Claude's AskUserQuestion, and **permission requests** (canUseTool) are the Claude's AskUserQuestion, and **permission requests** (canUseTool) are the
same shape with approve/deny options. Answered via one endpoint. 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. - `Status { state }` — idle / running / awaiting-input / compacting / exited.
- `UsageDelta { tokens }` — per-turn token counts where the dialect reports - `UsageDelta { tokens }` — per-turn token counts where the dialect reports
them (both do). them (both do).
@@ -244,11 +252,17 @@ everything spawning one created.
ANSI), encoding `aiapp://enroll?host=…&port=…&token=…`. The CA stays ANSI), encoding `aiapp://enroll?host=…&port=…&token=…`. The CA stays
embedded in the APK (`PinnedCert.kt` pattern), so the QR carries no embedded in the APK (`PinnedCert.kt` pattern), so the QR carries no
trust material — photographing the terminal leaks only the token 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 - **Storage**: server keeps only the SHA-256 in `config.json` (plain hash
is enough for high-entropy random input; buys that a leaked config is enough for high-entropy random input; buys that a leaked config
doesn't leak the credential). No "show token again" — lost means rotate. 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 - **Transport**: `Authorization: Bearer` header on every request including
the SSE GET. Never a query parameter (URLs leak into logs). The tracing 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 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 exists even inside the tunnel, so the token can't travel unencrypted by
misconfiguration, and interface binding failing closed (refuse to start 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 if `wg0` is absent, rather than falling back to 0.0.0.0) is part of the
same guarantee. same guarantee. Development gets `--bind <ip>` 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 - The bootstrap-over-HTTP trick from the updater is unnecessary here — the
app installs via Local Updater. app installs via Local Updater.
@@ -368,11 +385,16 @@ window just fills.
## Phases ## Phases
1. **Skeleton** — repo layout, cert script, TLS + token auth, wg0-bound 1. **Skeleton** — *done 2026-08-24.* Repo layout, cert script, TLS + token
listener (fail closed if the interface is missing), config.json, auth, wg0-bound listener (fail closed if the interface is missing),
session registry with a fake `EchoDriver`, session list + session screen in config.json, session registry with a fake `EchoDriver`, session list +
the app end-to-end over SSE. Proves the whole pipe before any AI is session screen in the app end-to-end over SSE. Proves the whole pipe
involved. 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, 2. **Claude local** — ClaudeDriver: spawn, stream text/tools, mid-run send,
interrupt, permission questions, AskUserQuestion, images both ways, delete. interrupt, permission questions, AskUserQuestion, images both ways, delete.
*Milestone: daily-drivable Claude replacement on localhost.* *Milestone: daily-drivable Claude replacement on localhost.*