Files
hack-house/docs/termux-operator.md
T
leetcrypt e97a45b78d docs(mobile): operator quickstart — hh launcher, direnv, web console, capture, skills
User-facing how-to distinct from the design spec: the port-probe reachability
gate, one-time Termux setup + tar-over-ssh sync, the hh launcher grammar,
per-directory direnv auto-host, the mobile web console, phone-capture.sh routes
(non-root file/term/webshot vs ADB shot/screen), and the Fairphone skill
ship-list. Linked from termux-operator.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-07 22:21:30 -07:00

13 KiB

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.

Operators: for the day-to-day how-to (the hh launcher, direnv auto-host, the mobile web console, and pulling captures back to the laptop) see mobile-quickstart.md. This document is the design spec.


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

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)

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

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:
    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):

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/):

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):

$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.

$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):

[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"
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-locks 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:

# 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.