ai-app: a phone interface to Claude Code and llama.cpp sessions
A Rust backend that owns the sessions and an Android app that reads them. The server spawns and adopts CLI processes, normalises everything they emit into one event model, keeps the transcript, and serves it over pinned TLS on a WireGuard interface; the phone streams that, replies, sends images, and imports conversations the machine already has. `AGENTS.md` is the working guide -- what runs where, what has been measured, and the faults that were expensive to find. `PLAN.md` is the design record. History before this point was squashed away. It was a personal project's running commentary and carried a name and a couple of machine paths that have no business in a public repository; the tree is what mattered and the tree is here.
This commit is contained in:
commit
b172c464ea
100 files changed
+31795
No files matched your search
Executable
+154
@@ -0,0 +1,154 @@
|
||||
#!/bin/sh
|
||||
# Sets up the WireGuard tunnel on the BACKEND HOST -- the machine that runs
|
||||
# ai-server and that the phone dials in to. Run this on the host, not in the
|
||||
# dev VM (the VM is behind qemu user-mode networking and has no inbound path;
|
||||
# see AGENTS.md).
|
||||
#
|
||||
# sudo WG_ENDPOINT=your-name.duckdns.org ./wg-setup-host.sh
|
||||
#
|
||||
# What it creates:
|
||||
# /etc/wireguard/wg0.conf the backend's tunnel: 10.66.0.1, port 51820
|
||||
# /etc/wireguard/peers/phone.conf the phone's config, shown as a QR to scan
|
||||
# and brings the interface up with wg-quick. Making it come back after a
|
||||
# reboot is left to you: that is the one step whose commands differ per init
|
||||
# system, and this script would only be guessing (the backend host is Gentoo,
|
||||
# the dev VM is Arch). It prints what to run at the end.
|
||||
#
|
||||
# Addressing matches PLAN.md: the phone reaches the backend at 10.66.0.1 from
|
||||
# everywhere, home or away -- one address in the app, one SAN in the leaf
|
||||
# certificate, no home/away distinction. The phone's AllowedIPs is only
|
||||
# 10.66.0.0/24, so this is a split tunnel: the phone's other traffic does not
|
||||
# route through your house, and nothing here forwards or NATs.
|
||||
#
|
||||
# Re-running is safe: existing keys are reused, so the phone's config stays
|
||||
# valid. Pass WG_NEW_PHONE_KEY=1 to issue a fresh phone keypair, which
|
||||
# invalidates the old one.
|
||||
#
|
||||
# The one thing this cannot do for you: forward UDP 51820 from your router to
|
||||
# this host. That is the only internet-facing hole, and it is silent to
|
||||
# unauthenticated packets -- scanners see a closed port.
|
||||
|
||||
set -eu
|
||||
|
||||
WG_DIR=/etc/wireguard
|
||||
PEER_DIR="$WG_DIR/peers"
|
||||
SERVER_IP=10.66.0.1
|
||||
PHONE_IP=10.66.0.2
|
||||
SUBNET=10.66.0.0/24
|
||||
PORT="${WG_PORT:-51820}"
|
||||
ENDPOINT="${WG_ENDPOINT:-}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run this with sudo -- it writes $WG_DIR and enables a service." >&2
|
||||
exit 1
|
||||
fi
|
||||
for tool in wg wg-quick; do
|
||||
command -v "$tool" >/dev/null || { echo "$tool not found: install wireguard-tools." >&2; exit 1; }
|
||||
done
|
||||
if [ -z "$ENDPOINT" ]; then
|
||||
echo "Set WG_ENDPOINT to the hostname the phone should dial from outside," >&2
|
||||
echo "e.g. WG_ENDPOINT=your-name.duckdns.org (a DDNS name, since a home IP" >&2
|
||||
echo "can change). Then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
mkdir -p "$PEER_DIR"
|
||||
|
||||
# Keys are generated here and never leave, except the phone's -- which is
|
||||
# what the QR carries. Regenerating the server key would invalidate every
|
||||
# peer, so it is created once and then reused.
|
||||
if [ ! -f "$WG_DIR/server.key" ]; then
|
||||
echo "==> Generating the backend's keypair"
|
||||
wg genkey > "$WG_DIR/server.key"
|
||||
wg pubkey < "$WG_DIR/server.key" > "$WG_DIR/server.pub"
|
||||
else
|
||||
echo "==> Reusing the backend's existing keypair"
|
||||
fi
|
||||
if [ ! -f "$PEER_DIR/phone.key" ] || [ -n "${WG_NEW_PHONE_KEY:-}" ]; then
|
||||
echo "==> Generating the phone's keypair"
|
||||
wg genkey > "$PEER_DIR/phone.key"
|
||||
wg pubkey < "$PEER_DIR/phone.key" > "$PEER_DIR/phone.pub"
|
||||
else
|
||||
echo "==> Reusing the phone's existing keypair"
|
||||
fi
|
||||
|
||||
echo "==> Writing $WG_DIR/wg0.conf"
|
||||
cat > "$WG_DIR/wg0.conf" <<EOF
|
||||
# Generated by ai-app/wg-setup-host.sh. The backend binds this interface's
|
||||
# address and refuses to start without it (see server/src/main.rs).
|
||||
[Interface]
|
||||
Address = $SERVER_IP/24
|
||||
ListenPort = $PORT
|
||||
PrivateKey = $(cat "$WG_DIR/server.key")
|
||||
|
||||
[Peer]
|
||||
# phone
|
||||
PublicKey = $(cat "$PEER_DIR/phone.pub")
|
||||
AllowedIPs = $PHONE_IP/32
|
||||
EOF
|
||||
|
||||
echo "==> Writing $PEER_DIR/phone.conf"
|
||||
cat > "$PEER_DIR/phone.conf" <<EOF
|
||||
[Interface]
|
||||
Address = $PHONE_IP/24
|
||||
PrivateKey = $(cat "$PEER_DIR/phone.key")
|
||||
|
||||
[Peer]
|
||||
PublicKey = $(cat "$WG_DIR/server.pub")
|
||||
Endpoint = $ENDPOINT:$PORT
|
||||
# Split tunnel: only the backend's subnet goes over WireGuard.
|
||||
AllowedIPs = $SUBNET
|
||||
# Keeps the mapping alive through home NAT so the backend can reach the
|
||||
# phone first (needed later for "your turn" push).
|
||||
PersistentKeepalive = 25
|
||||
EOF
|
||||
|
||||
if wg show wg0 >/dev/null 2>&1; then
|
||||
# Already up: load the new peers without dropping the interface, so a
|
||||
# re-run doesn't kill a connected phone mid-session. `wg-quick strip`
|
||||
# prints the config with the wg-quick-only keys removed, which is what
|
||||
# `wg syncconf` accepts.
|
||||
echo "==> wg0 is already up -- reloading its peers in place"
|
||||
STRIPPED=$(mktemp)
|
||||
trap 'rm -f "$STRIPPED"' EXIT
|
||||
wg-quick strip wg0 > "$STRIPPED"
|
||||
wg syncconf wg0 "$STRIPPED"
|
||||
else
|
||||
echo "==> Bringing wg0 up"
|
||||
wg-quick up wg0
|
||||
fi
|
||||
sleep 1
|
||||
wg show wg0 | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "==> Phone config -- scan this with the WireGuard app (Add > Scan from QR code):"
|
||||
echo
|
||||
if command -v qrencode >/dev/null; then
|
||||
qrencode -t ansiutf8 < "$PEER_DIR/phone.conf"
|
||||
else
|
||||
echo " (install qrencode to get a scannable QR; the config is below)"
|
||||
sed 's/^/ /' "$PEER_DIR/phone.conf"
|
||||
fi
|
||||
echo
|
||||
echo "Still to do, in order:"
|
||||
echo " 0. Make wg0 come back after a reboot. Left to you rather than"
|
||||
echo " guessed at, since the command depends on your init system:"
|
||||
echo " OpenRC: ln -s /etc/init.d/wg-quick /etc/init.d/wg-quick.wg0"
|
||||
echo " rc-update add wg-quick.wg0 default"
|
||||
echo " systemd: systemctl enable wg-quick@wg0"
|
||||
echo " (Gentoo with netifrc instead of wg-quick: configure net.wg0 in"
|
||||
echo " /etc/conf.d/net -- see the WireGuard page on the Gentoo wiki.)"
|
||||
echo " 1. Forward UDP $PORT on your router to this host. That is the only"
|
||||
echo " internet-facing port; it stays silent to unauthenticated packets."
|
||||
echo " 2. Point $ENDPOINT at your home IP (DDNS client on the router, or a"
|
||||
echo " curl cron here). WireGuard on the phone resolves this once when the"
|
||||
echo " tunnel comes up, so after a rare IP change, toggle the tunnel."
|
||||
echo " 3. Check NAT hairpinning works at home: with the tunnel on and the"
|
||||
echo " phone on your wifi, 'ping $SERVER_IP' from the phone should answer."
|
||||
echo " If it doesn't, your router can't hairpin -- turn the tunnel off at"
|
||||
echo " home, or use a split-DNS entry pointing $ENDPOINT at the LAN IP."
|
||||
echo " 4. Start the backend here (it binds $SERVER_IP only, and refuses to"
|
||||
echo " start if wg0 is down):"
|
||||
echo " cd $(dirname "$(readlink -f "$0")") && ./server/target/release/ai-server"
|
||||
echo " Add --rotate-token once to print a fresh enrollment QR for the app."
|
||||
Reference in new issue
Block a user