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:
1 parent
213bc72b64
commit
f3cebeea78
2 files changed
+73
-24
No files matched your search
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.*
|
||||||
|
|||||||
Reference in new issue
Block a user