From 56491f84b001d8c449467bda26c196fae4c80446 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Tue, 25 Aug 2026 05:10:20 -0400 Subject: [PATCH] Follow the sibling project's rename to dev-updater It is no longer "local" -- it serves over WireGuard rather than the LAN -- and it is specifically for developing new apps. Renaming the references here at the same time keeps one name to search for across both repos. Also drops the last references to gen-dev-cert.sh, which the in-process certificate generation replaced: the build script and the Gradle task now say to start the server once, and test-wg-tunnel.sh reads the certificates from the XDG directory rather than the repo. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw --- AGENTS.md | 14 ++++++------- PLAN.md | 20 +++++++++---------- app/androidApp/build.gradle.kts | 6 +++--- app/androidApp/src/main/AndroidManifest.xml | 2 +- .../kotlin/com/example/aiapp/MainActivity.kt | 2 +- .../com/example/aiapp/SessionListScreen.kt | 2 +- app/build-apk.sh | 17 ++++++++-------- app/run-android.sh | 2 +- server/src/certs.rs | 2 +- server/src/main.rs | 2 +- server/src/session/mod.rs | 2 +- test-wg-tunnel.sh | 12 +++++------ 12 files changed, 42 insertions(+), 41 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 151e194..79aced0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,11 +22,11 @@ session-type branch in shared code (routes, transcript, app screens). ## Layout -Mirrors `../local-updater` deliberately — same stack (axum 0.8 + +Mirrors `../dev-updater` deliberately — same stack (axum 0.8 + axum-server/rustls, tokio, clap; Kotlin 2.4.x + Compose Multiplatform, single `:androidApp` module), same cert scheme, same registry pattern (every session mutation funnels through the manager so in-memory and on-disk state -can't come apart). Read local-updater's `README.md` and `AGENTS.md` for the +can't come apart). Read dev-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. @@ -62,7 +62,7 @@ real-phone/WireGuard bring-up, which is operational rather than code. from `server/` — the build stays warning-clean, keep it that way. - App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin` to typecheck; `./build-apk.sh` to produce the APK to install on a phone - (through Local Updater); `./run-android.sh` to build, install, and launch + (through Dev Updater); `./run-android.sh` to build, install, and launch on the emulator. - **The APK pins the CA of the machine that builds it**, read at build time from `$XDG_CONFIG_HOME/ai-app/certs/ca.pem` (`AI_APP_CA` overrides) and @@ -83,7 +83,7 @@ real-phone/WireGuard bring-up, which is operational rather than code. Established 2026-08-25, and it decides more than it looks like: - **The host (192.168.1.168) is the backend machine.** It runs - local-updater's server today and is where `ai-server` belongs in + dev-updater's server today and is where `ai-server` belongs in production: it has the LAN address the phone can reach, and it's where WireGuard terminates. `wg-setup-host.sh` sets that up (keys, `wg0.conf`, the phone's QR); run it there with `sudo WG_ENDPOINT=`. @@ -157,10 +157,10 @@ Established 2026-08-25, and it decides more than it looks like: SourceDirectory(task, Task::outputDir) }`, which also carries the task dependency. -## Environment notes (this machine, learned in local-updater) +## Environment notes (this machine, learned in dev-updater) - Android SDK is at `~/Android/Sdk`, not the root-owned `/opt/android-sdk` - the ambient `$ANDROID_HOME` may point at; copy local-updater's + the ambient `$ANDROID_HOME` may point at; copy dev-updater's `android-env.sh` override pattern. - Each agent command runs in a fresh shell — exported environment does not carry over. Chain: `cd app && . ./android-env.sh && ./gradlew …`. Never @@ -171,5 +171,5 @@ Established 2026-08-25, and it decides more than it looks like: (`setsid nohup … & disown -h`, verify `PPID 1`), and every `pgrep -f`/`pkill -f` pattern needs its first character bracketed (`[a]i-server`) in **every** occurrence in the command, or the pattern - matches the shell running it. Full explanation in local-updater's + matches the shell running it. Full explanation in dev-updater's `AGENTS.md` — it bites exactly the same way here. diff --git a/PLAN.md b/PLAN.md index 02ac20a..a92f7f7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -7,7 +7,7 @@ the official app gets wrong (e.g. it won't deliver a typed message until the session fully finishes its turn, where the TUI injects it at the next tool boundary). -Same shape as `../local-updater`: a Rust (Axum) backend on the desktop, a +Same shape as `../dev-updater`: a Rust (Axum) backend on the desktop, a Kotlin/Compose Android app, pinned self-signed TLS between them. ## The one idea everything hangs off @@ -81,12 +81,12 @@ backend (Rust/Axum, desktop) ### Backend layout (`server/`) -Mirroring local-updater's stack: axum 0.8, axum-server + rustls, tokio, serde, +Mirroring dev-updater's stack: axum 0.8, axum-server + rustls, tokio, serde, clap, tracing. Rust edition 2024, warning-clean, clippy in CI habit. - `main.rs` — bootstrap, TLS listener. - `routes.rs` — the whole HTTP table in one module doc comment (as in - local-updater). + dev-updater). - `session/mod.rs` — `SessionManager`: the live session registry, every mutation funnels through it (the `registry.rs` pattern: in-memory and on-disk state can't come apart). @@ -260,14 +260,14 @@ complete path out of everything spawning one created. ### Security - TLS with a self-signed CA, pinned in the app — same - idempotent-CA/reissued-leaf scheme as local-updater, same one-way-door + idempotent-CA/reissued-leaf scheme as dev-updater, same one-way-door caveat about regenerating the CA, but generated **in process on first start** (`certs.rs`) rather than by a shell script calling openssl (2026-08-25). One place then decides the extensions, the file modes, and which addresses the leaf covers — every local IPv4 plus loopback and the emulator's host alias, so nobody maintains a hardcoded IP — and there is no setup step to forget. - - Unlike local-updater, the pinned CA is **not a constant in the source**: + - Unlike dev-updater, the pinned CA is **not a constant in the source**: the build reads `$XDG_CONFIG_HOME/ai-app/certs/ca.pem` from the machine doing the build and generates the constant (`generatePinnedCert` in `app/androidApp/build.gradle.kts`; `AI_APP_CA` overrides). Decided @@ -392,12 +392,12 @@ complete path out of everything spawning one created. 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. + app installs via Dev Updater. ## App (`app/`) Kotlin + Compose Multiplatform, single `:androidApp` module, same versions as -local-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens: +dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens: 1. **Session list** — cards: kind icon, title, host, model, status (running / awaiting answer / idle / exited), last activity. Spawn FAB; @@ -420,7 +420,7 @@ local-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens: 5. **Settings** — server address + token, hosts editor, llama model list editor. -Networking mirrors local-updater's app layer (`AppsApi.kt` style thin client + +Networking mirrors dev-updater's app layer (`AppsApi.kt` style thin client + pinned transport), plus an SSE client with `after=` resume driven by connectivity/lifecycle. The app keeps no persistent transcript store — the backend's transcript is the source of truth; the app caches only for the @@ -501,7 +501,7 @@ window just fills. Each phase ends runnable and verified against the real thing (rule 22); the backend gets tests where logic is pure (event normalization, transcript cursors, config persistence, refcounting) — the app is UI over the API and is -verified by running it, matching local-updater's posture. +verified by running it, matching dev-updater's posture. ## Open questions / risks @@ -539,6 +539,6 @@ installed versions when each phase starts): ≥180 s polling; wrong User-Agent → aggressive 429 bucket): https://github.com/anthropics/claude-code/issues/31637 and https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/202 -- Sibling project this repo's conventions mirror: `../local-updater` +- Sibling project this repo's conventions mirror: `../dev-updater` (README.md + AGENTS.md — server/registry/routes layout, cert scheme, testing posture, Android env notes). diff --git a/app/androidApp/build.gradle.kts b/app/androidApp/build.gradle.kts index 09b73dc..e0697c2 100644 --- a/app/androidApp/build.gradle.kts +++ b/app/androidApp/build.gradle.kts @@ -6,7 +6,7 @@ plugins { // The CA this app pins is baked in at build time from the certificates on // the machine doing the build -- `$XDG_CONFIG_HOME/ai-app/certs/ca.pem`, -// as written by ../gen-dev-cert.sh. AI_APP_CA overrides the path. +// which the server generates on first start. AI_APP_CA overrides the path. // // Reading it rather than keeping a pasted copy in the source is what makes // the trust boundary follow the build: an APK built on the backend host @@ -45,8 +45,8 @@ abstract class GeneratePinnedCert : DefaultTask() { if (!ca.isFile) { throw GradleException( "No CA certificate at $path.\n" + - "Run ./gen-dev-cert.sh on this machine first -- the app pins the CA it " + - "generates, and the certificate has to exist before an APK can embed it.\n" + + "Start ai-server once on this machine first -- it generates the CA the " + + "app pins, and the certificate has to exist before an APK can embed it.\n" + "Set AI_APP_CA=/path/to/ca.pem to build against a different one.", ) } diff --git a/app/androidApp/src/main/AndroidManifest.xml b/app/androidApp/src/main/AndroidManifest.xml index c2e43e1..775dd36 100644 --- a/app/androidApp/src/main/AndroidManifest.xml +++ b/app/androidApp/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ network address, including a plain socket to a LAN IP literal. Without it the traffic is silently dropped, surfacing only as a connect timeout. See MainActivity.kt's runtime request, and - local-updater's manifest for the full story. --> + dev-updater's manifest for the full story. --> /dev/null \ | openssl pkey -pubin -outform der 2>/dev/null \ | openssl dgst -sha256 -binary 2>/dev/null \ @@ -49,8 +50,8 @@ if [ -f "$CA" ]; then echo "==> Pinning the CA at $CA" echo " fingerprint: $FINGERPRINT" else - echo "No CA certificate at $CA -- run ../gen-dev-cert.sh on this machine" >&2 - echo "first, or set AI_APP_CA to one. The APK has to embed it at build time." >&2 + echo "No CA certificate at $CA -- start ai-server once on this machine" >&2 + echo "(it generates them), or set AI_APP_CA. The APK embeds it at build time." >&2 exit 1 fi @@ -62,7 +63,7 @@ echo echo "==> Built $APK" [ -f "$APK" ] && ls -lh "$APK" | awk '{print " " $5}' echo -echo "To get it onto the phone: add this project to Local Updater (or hit" +echo "To get it onto the phone: add this project to Dev Updater (or hit" echo "Update on it if it's already there) and install from there." echo "Then start the backend and scan the enrollment QR it prints:" echo " ./server/target/release/ai-server --rotate-token" diff --git a/app/run-android.sh b/app/run-android.sh index c85e9f5..3bc9693 100755 --- a/app/run-android.sh +++ b/app/run-android.sh @@ -1,6 +1,6 @@ #!/bin/sh # Builds and runs this app on an emulator, creating/booting the AVD first if -# it isn't already up. Same flow as local-updater's run-android.sh; see that +# it isn't already up. Same flow as dev-updater's run-android.sh; see that # script for the reasoning behind the avd handling. # # Environment setup (SDK location, PATH, ...) lives in ./android-env.sh, diff --git a/server/src/certs.rs b/server/src/certs.rs index 7f7b5ed..4225eaa 100644 --- a/server/src/certs.rs +++ b/server/src/certs.rs @@ -111,7 +111,7 @@ fn generate_leaf( params.distinguished_name.push(DnType::OrganizationName, "ai-app dev"); params.distinguished_name.push( DnType::CommonName, - addresses.first().map(|a| a.to_string()).unwrap_or_else(|| "local-updater".to_string()), + addresses.first().map(|a| a.to_string()).unwrap_or_else(|| "dev-updater".to_string()), ); params.subject_alt_names = addresses.iter().map(|a| SanType::IpAddress(*a)).collect(); params.is_ca = IsCa::ExplicitNoCa; diff --git a/server/src/main.rs b/server/src/main.rs index ee2d5ed..77a29fa 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -193,7 +193,7 @@ async fn main() -> Result<()> { tracing::warn!( "a new CA was generated in {} -- any installed app pins the previous one and can no \ longer reach this server. Rebuild it with app/build-apk.sh, which embeds this CA, \ - and reinstall through Local Updater.", + and reinstall through Dev Updater.", certs_dir.display(), ); } diff --git a/server/src/session/mod.rs b/server/src/session/mod.rs index 7984557..75289b0 100644 --- a/server/src/session/mod.rs +++ b/server/src/session/mod.rs @@ -1,7 +1,7 @@ //! The live session registry. Every session mutation -- spawn, delete, //! token changes -- funnels through [`SessionManager`] under one lock, so //! in-memory state and `config.json` can't come apart (the same pattern as -//! local-updater's `registry.rs`). +//! dev-updater's `registry.rs`). //! //! A live session is a driver plus one event pump: the driver reports //! [`Event`]s into an mpsc channel; the pump assigns each a sequence diff --git a/test-wg-tunnel.sh b/test-wg-tunnel.sh index b7ac7ad..e0e2fd4 100755 --- a/test-wg-tunnel.sh +++ b/test-wg-tunnel.sh @@ -14,8 +14,8 @@ # The veth pair stands in for "the internet" carrying WireGuard's UDP; the # wg interfaces are real, with a real handshake and real keys. 10.66.0.1 is # deliberately the same address the leaf certificate carries a SAN for -# (gen-dev-cert.sh), so a client inside the tunnel completes the same -# pinned-TLS handshake a phone will. +# (certs.rs covers every local address), so a client inside the tunnel +# completes the same pinned-TLS handshake a phone will. # # ./test-wg-tunnel.sh up create the tunnel (needs sudo) # ./test-wg-tunnel.sh test run the server on wg0 and reach it from "phone" @@ -80,8 +80,9 @@ up() { } test_tunnel() { - if [ ! -f "$REPO/certs/leaf.pem" ]; then - echo "No certs/ -- run ./gen-dev-cert.sh first." >&2 + CERTS="${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs" + if [ ! -f "$CERTS/leaf.pem" ]; then + echo "No certificates in $CERTS -- start ai-server once; it makes them." >&2 exit 1 fi if [ ! -x "$REPO/server/target/debug/ai-server" ]; then @@ -98,13 +99,12 @@ test_tunnel() { ss -tlnp 2>/dev/null | grep 8443 | sed 's/^/ /' || echo " (nothing on 8443)" echo "==> From inside the tunnel: GET /sessions through wg1 -> wg0" - TOKEN=$(sudo cat "$REPO/config.json" 2>/dev/null | sed -n 's/.*"sha256": "\(.*\)".*/\1/p' | head -1) if [ -z "${AI_TOKEN:-}" ]; then echo " (set AI_TOKEN= to test an authorized call;" echo " without it this only proves reachability + TLS, via a 401)" fi sudo ip netns exec "$NS" curl -s -o /dev/null -w " HTTP %{http_code} (TLS ok, pinned CA)\n" \ - --cacert "$REPO/certs/ca.pem" \ + --cacert "$CERTS/ca.pem" \ ${AI_TOKEN:+-H "Authorization: Bearer $AI_TOKEN"} \ "https://$SERVER_WG_IP:8443/sessions" || echo " UNREACHABLE"