docs: spec for running a hack-house operator from Android/Termux

Phased plan (P0 dependency spike → P1 read-only presence → P2 sandbox drive →
P3 remote-provider autonomy → P4 persistence) for joining a room as a user from
Termux. Grounded in the real SRP/Fernet/ws client and the operate provider path.
Covers the srp aarch64 build risk + pure-Python SRP fallback, the cryptography
pin workaround, and Tailscale / reverse-SSH / port-forward reachability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-07-07 14:58:38 -07:00
parent 7afc38232b
commit 88e5dfa0cb
+289
View File
@@ -0,0 +1,289 @@
# Spec — hack-house operator from Android / Termux
**Status:** planning · **Owner:** andre · **Drafted:** 2026-07-07
**Goal:** run a hack-house *operator/user* from a phone (Termux, aarch64 Android)
that can **join a room as a first-class member** — read, chat, and drive a
room-owned shared sandbox — with autonomy driven by a **remote model provider**
(no on-device model). Reachability via **Tailscale** first, with port-forward and
reverse-SSH documented as alternates.
Target device is the Murena Fairphone 6 (Android 15/16 base) running Termux. A
future postmarketOS path (full Linux userland) is noted in Appendix C.
---
## 1. Why this is tractable
The operator side is **pure Python** and, for everything except `sbx launch`,
needs **only a network connection** — the shared sandbox lives on the server
host, and the operator drives it by relaying keystrokes/reads over the room
websocket. So Android's lack of podman/systemd/X11 does **not** block us.
Grounding (verified in-repo):
| Concern | Reality | Source |
|---|---|---|
| Operator↔room transport | SRP-auth HTTP handshake → Fernet-encrypted websocket | `cmd_chat/client/client.py:106-157,502-505` |
| Operator reuses the same client | `from ..client.client import Client` | `cmd_chat/operator/bridge.py:27` |
| TLS is optional | `--no-tls` (ws/http), `--insecure` (skip cert verify) | `cmd_chat/client/client.py:41,69-89` |
| Sandbox drive needs no local engine | `keys`/`screen`/`watch` relay frames to the broker-owned PTY | bridge `keys`/`screen` verbs |
| Only `sbx launch` needs podman | operator's *own* container path; skip it on Android | bridge sandbox target logic |
| Autonomy via remote model | `operate --profile NAME` \| `--provider SPEC --model M` | `cmd_chat/operator/__main__.py:113-123,408-436` |
| Model profiles file | `models.toml`, secrets via `api_key_env` | `cmd_chat/ai/profiles.py:1-70` |
**Non-goals (this spec):** running a local LLM on-device; `sbx launch` /
operator-owned containers on Android; the Rust TUI on Android; recursive
`spawn` of `claude -p` on-device (needs Node; deferred, see Phase 3B).
---
## 2. Phase 0 — Dependency spike (the risk gate)
**Objective:** prove the operator's Python deps install & import in Termux on
aarch64. This is the make-or-break step; everything else is wiring.
### 0.1 Base packages
```bash
pkg update && pkg upgrade
pkg install python openssl clang rust libffi make git
python -V # expect 3.11+ (gives stdlib tomllib for models.toml)
```
### 0.2 Easy deps (pure-Python or aarch64 wheels)
```bash
pip install requests rich websockets
python -c "import requests, rich, websockets; print('ok')"
```
Expected: clean. `websockets` ships aarch64 wheels; `requests`/`rich` are pure
Python.
### 0.3 The two hard deps
```bash
pip install "cryptography" # see 0.4 on the >=46 pin
pip install srp # <-- most likely failure
python -c "import srp; from cryptography.fernet import Fernet; print('ok')"
```
### 0.4 `cryptography>=46.0.0` pin
`requirements.txt` pins `cryptography>=46.0.0`. Two outcomes:
- **Rust build succeeds** (we installed `rust`+`openssl`) → done.
- **Build too heavy / OOM on device** → install Termux's packaged build and
relax the pin *for the operator install only*:
```bash
pkg install python-cryptography
python -c "import cryptography, sys; print(cryptography.__version__)"
```
If the packaged version is `< 46`, confirm the operator actually needs the
floor. It uses only `cryptography.fernet.Fernet` (`client.py:12`), stable for
years — so a lower version is almost certainly fine. **Action if needed:** add
an extras/loosened constraint (e.g. `requirements-operator.txt` with
`cryptography>=41`) rather than editing the server's pin. Do **not** weaken the
server requirement.
### 0.5 `srp==1.0.22` — the gate + fallback
`srp` is a C extension with no reliable aarch64 wheel. If `pip install srp`
fails to build under Termux, the fix is a **pure-Python SRP** shim behind the
exact interface `client.py` already uses.
**Interface to preserve** (from `cmd_chat/client/client.py`):
```python
srp.rfc5054_enable() # module-level, line 19
usr = srp.User(b"chat", password, hash_alg=srp.SHA256) # line 106
_, A = usr.start_authentication() # line 107 -> (username, A_bytes)
M = usr.process_challenge(salt, B) # line 134 -> M_bytes (or None on failure)
usr.verify_session(H_AMK) # line 152 -> raises/None on bad server proof
usr.authenticated() # -> bool (guard after verify)
```
Server endpoints consumed: `POST /srp/…` (challenge) and `POST /srp/verify`
(`client.py:109,139-157`). The wire params are `salt`, `B`, `A`, `M`, `H_AMK`,
`user_id`, `ws_token` — a pure-Python SRP-6a (RFC 5054 group, SHA-256) must
reproduce the **same** N/g group, `k`, `x`, `u`, and `M1`/`H_AMK` derivation as
the `srp` package with `rfc5054_enable()`.
**Fallback plan (code change, only if 0.5 build fails):**
1. Vendor a small `cmd_chat/client/_srp_pure.py` implementing SRP-6a (RFC 5054
2048-bit group, SHA-256) exposing a drop-in `rfc5054_enable`, `User`,
`SHA256`, matching the five calls above.
2. In `client.py`, `try: import srp` / `except ImportError: from . import
_srp_pure as srp`. No other call-site changes.
3. **Interop test is mandatory** (server uses the real `srp`): run the pure
client against a real server room and confirm auth succeeds — SRP is
unforgiving about constant/hash mismatches.
**Phase 0 acceptance:** on the phone, `python -c "import srp, websockets,
requests, rich; from cryptography.fernet import Fernet; print('ok')"` prints
`ok` (via wheel/build **or** the pure-Python shim).
---
## 3. Phase 1 — Read-only presence (transport proof)
**Objective:** join a live room from the phone; prove SRP + Fernet + ws over the
network. No sandbox.
Layout on device (clone or rsync the repo; operator only needs `cmd_chat/`):
```bash
git clone <gitea>/hacker-house && cd hacker-house
python -m venv .venv && . .venv/bin/activate
pip install requests rich websockets cryptography # + srp or the shim
HH=".venv/bin/python -m cmd_chat.operator"
```
Join + observe + speak (over Tailscale, `--no-tls` acceptable — see App. A):
```bash
$HH up <server-tailscale-host> <port> phone-op --password <pw> --no-tls
$HH read --wait --timeout 30
$HH say "phone operator online"
$HH roster ; $HH status
$HH down
```
**Acceptance:** roster shows `phone-op`; `say` appears in the room; `read`
streams room events as JSONL. Daemon control socket lands under
`$XDG_RUNTIME_DIR` → in Termux falls back to `$PREFIX/tmp/hh-bridge/<session>/`
(AF_UNIX works there — verify `control.sock` is created).
---
## 4. Phase 2 — Sandbox drive (core operator loop)
**Objective:** drive a **room-owned** sandbox from the phone with no local
container engine.
```bash
$HH keys "uname -a" enter
$HH screen # read the PTY buffer back
$HH watch --for 'PASS|FAIL' --in screen --timeout 60
$HH exec "ls /root" # only if the room sandbox is co-located & granted
```
**Skip** `sbx launch` (wants podman). If a verb reports "no sandbox / not a
granted driver", that's the grant model working — the room must `/grant
phone-op` first.
**Acceptance:** keystrokes reach the shared PTY, `screen` returns output,
`watch` resolves a stop-condition — all from the phone.
---
## 5. Phase 3 — Autonomy via a remote provider
**Objective:** let a **remote** model be the operator brain — no on-device
inference.
### 3A (primary) — `operate` with a remote provider
Create `~/.config/hh/models.toml` (secrets via env, never in the file):
```toml
[claude]
provider = "anthropic"
model = "claude-sonnet-4-6"
api_key_env = "ANTHROPIC_API_KEY"
[groq-llama]
provider = "openai" # OpenAI-compatible endpoint
base_url = "https://api.groq.com/openai/v1"
model = "llama-3.3-70b-versatile"
api_key_env = "GROQ_API_KEY"
```
```bash
export ANTHROPIC_API_KEY=… # in the Termux session / profile
$HH operate --profile claude # or: $HH operate --provider openai --model … --base-url …
```
Resolution + a `preflight` credential/endpoint check happen in
`_build_operate_provider` (`__main__.py:408-436`); a bad/missing key fails fast
with a clear message. `models.toml` lookup order:
`$HH_MODELS_FILE` → `./models.toml` → `~/.config/hh/models.toml`
(`ai/profiles.py:34-43`).
**Acceptance:** `operate --profile claude` runs the read→think→act loop against
the room using a cloud model; the phone stays cool (no local weights).
### 3B (deferred) — recursive `spawn`
`spawn` shells out to `claude -p` (needs Node + `@anthropic-ai/claude-code`).
Heavy on-device; out of scope for v1. Revisit only if the phone becomes a
persistent autonomous node (or under postmarketOS — App. C).
---
## 6. Phase 4 — Persistence & UX on Android
Android Doze/OOM will reap a detached daemon. Mitigations:
- **`termux-wake-lock`** before `up` (release with `termux-wake-unlock` on stop).
Requires the Termux:API addon.
- **Termux:Boot** addon → a boot script that `wake-lock`s and runs the operator,
for auto-start after reboot.
- **Wrapper script** `~/bin/hh-phone` capturing host/port/name/password (from
Termux secure storage or a `chmod 600` env file — never commit) →
`termux-wake-lock; $HH up …; $HH operate --profile claude`.
- **Reconnect-on-drop:** mobile networks flap. Confirm the daemon's behaviour on
ws drop; if it exits, wrap `up`+`operate` in a supervised restart loop
(backoff). (Potential small code hardening — track separately.)
- **Battery:** on cellular + wake-lock this is a real drain; expect to run it
plugged in for long sessions.
**Acceptance:** operator survives screen-off / Doze for ≥30 min and auto-resumes
after a network blip.
---
## Appendix A — Connectivity (primary: Tailscale)
**Tailscale (chosen for now).** First-class Termux support. Put phone + server on
one tailnet; the server gets a stable MagicDNS name reachable from anywhere.
- Pros: zero port-forwarding, NAT-traversal handled, WireGuard-encrypted, ACLs.
- TLS: over the tailnet the link is already encrypted → **`--no-tls` is
acceptable** (SRP still authenticates and Fernet still encrypts room payloads
end-to-end). For belt-and-suspenders, use Tailscale MagicDNS + a cert and drop
`--no-tls`.
- Setup: install Tailscale on phone + server, `tailscale up` both, then
`$HH up <server-magicdns> <port> …`.
## Appendix B — Connectivity alternates
**B1. Port-forward (router).** Forward `server_port` on the home router to the
server host.
- Pros: no extra software; direct.
- Cons: exposes the port to the public internet — **must** use `wss://` (real
cert, drop `--no-tls`), a strong SRP password, and ideally an allowlist/geo
filter or fail2ban. Dynamic home IP → needs DDNS. Weakest security posture;
use only with TLS + strong auth.
**B2. Reverse-SSH tunnel.** Server opens an outbound SSH tunnel to a VPS that has
a public IP; phone connects to the VPS:
```bash
# on the server (autossh keeps it up):
autossh -M 0 -N -R 0.0.0.0:8443:localhost:<server_port> user@vps
# phone → $HH up <vps-host> 8443 …
```
- Pros: works behind CGNAT / no router access; only the VPS is exposed; can bind
the tunnel to the VPS loopback and reach it via a second SSH hop for zero
public exposure.
- Cons: needs a VPS; extra hop adds latency; manage `autossh`/keys. Bind to
`0.0.0.0` only with TLS, else prefer `localhost` + SSH-hop from the phone.
**Recommendation:** Tailscale for daily use; reverse-SSH (B2) as the fallback
when a tailnet isn't an option; public port-forward (B1) only as a last resort
and only with real TLS.
## Appendix C — Future: postmarketOS full-Linux path
Per the Fairphone-6 C2 notes, pmOS has a mainline FP6 port. On a full Linux
userland the calculus changes: real package manager, easier `cryptography`/`srp`
builds, Node for `spawn`, and — with a working container stack — even
operator-owned `sbx launch`. If the phone graduates to pmOS, this spec collapses
to "install like a normal Linux operator" and Phases 0/3B/skip-podman caveats
mostly evaporate.
---
## Risk register
| # | Risk | Likelihood | Mitigation |
|---|---|---|---|
| R1 | `srp` C-ext won't build in Termux | High | Pure-Python SRP shim (§0.5) + interop test vs real server |
| R2 | `cryptography>=46` too heavy to build | Med | `pkg install python-cryptography`; loosen pin in an operator-only reqs file (§0.4) |
| R3 | Daemon reaped by Doze/OOM | High | `termux-wake-lock`, Termux:Boot, supervised restart (§Phase 4) |
| R4 | Public exposure via port-forward | Med | Prefer Tailscale/reverse-SSH; if forwarding, mandatory `wss://` + strong SRP (§App B1) |
| R5 | Mobile network flap drops ws | High | Reconnect/backoff wrapper; possible daemon hardening (§Phase 4) |
| R6 | SRP shim constants mismatch server | Med | Mirror RFC 5054 2048-bit group + SHA-256 exactly; gate on a live-auth test |
## Milestone acceptance summary
- **P0:** all operator deps import on device (wheel/build or shim).
- **P1:** phone appears in roster; can `read`/`say`.
- **P2:** phone drives a granted room sandbox via `keys`/`screen`/`watch`.
- **P3:** `operate --profile <remote>` runs the loop with a cloud model.
- **P4:** survives Doze + network blip, auto-resumes.