docs(mobile): harden Fairphone/Termux SSH operations doctrine
Encode the robust way to reach + drive the phone in CLAUDE.md and the hh-operator skill (which had no mobile section at all): - One canonical hardened ssh prefix (ConnectTimeout/BatchMode/accept-new) that never hangs and dodges the wrong-alias trap. - Reachability gate probes the TCP port, NOT `tailscale status` — the tailnet shows fairphone-6 "active" even when Android has killed backgrounded Termux and sshd is dead (observed live). - Fix-is-on-device guidance (termux-wake-lock) instead of retrying a dead port; Termux env quirks (no /tmp, no rustc, skip srp). - Status advanced: phone can both JOIN and HOST rooms (server-side SRP shim) + the stdlib mobile web console mirrors a real room. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,32 +31,45 @@ $HH spawn "<objective incl. the full join cmd>" --room-host … --go --skip-perm
|
||||
## Mobile operator on Android / Termux — the phone is REACHABLE, use it
|
||||
|
||||
Spec: `docs/termux-operator.md`. The operator side is pure Python and runs on
|
||||
the **Fairphone-6** in Termux. You can drive that device directly over SSH — so
|
||||
**verify reachability before ever telling the user "this can only run on-device,
|
||||
paste it back."** That framing is wrong when the phone is a live SSH host.
|
||||
the **Fairphone-6** in Termux — it can both **join** and **host** rooms (the SRP
|
||||
shim now covers the server side too, so no Rust / no `srp` C-ext). You can drive
|
||||
the device directly over SSH — so **verify reachability before ever telling the
|
||||
user "this can only run on-device, paste it back."** That framing is wrong when
|
||||
the phone answers SSH.
|
||||
|
||||
One canonical, hardened invocation (use this prefix everywhere — it sidesteps
|
||||
the wrong-alias trap and never hangs):
|
||||
```bash
|
||||
# Fairphone-6 Termux over Tailscale (verified 2026-07-07). sshd on port 8022.
|
||||
ssh -i ~/.ssh/phone-deploy -p 8022 u0_a203@100.95.202.68
|
||||
PHONE='ssh -i ~/.ssh/phone-deploy -p 8022 -o ConnectTimeout=8 -o BatchMode=yes -o StrictHostKeyChecking=accept-new u0_a203@100.95.202.68'
|
||||
$PHONE 'uname -m; python --version' # aarch64 / Python 3.13.x
|
||||
```
|
||||
|
||||
- Tailscale device `fairphone-6` = **`100.95.202.68`**, port **8022**, user
|
||||
**`u0_a203`**, key **`~/.ssh/phone-deploy`**. Confirm it's up first:
|
||||
`tailscale status | grep fairphone` and a TCP probe of `:8022`.
|
||||
- **Reachability gate — probe the PORT, not `tailscale status`:**
|
||||
`timeout 8 bash -c 'cat </dev/null >/dev/tcp/100.95.202.68/8022' && echo REACHABLE || echo OFFLINE`.
|
||||
Tailscale reports `fairphone-6` **`active` even when sshd is dead** — Android
|
||||
kills backgrounded Termux, taking `sshd` with it. The tailnet line is NOT
|
||||
proof; the TCP probe is. If OFFLINE, the fix is **on-device** (wake the phone,
|
||||
relaunch Termux, `termux-wake-lock` + start `sshd`) — say so, don't retry a
|
||||
dead port.
|
||||
- **Trap:** the `~/.ssh/config` aliases `phone` / `phone-deploy` point at
|
||||
`100.95.180.14` (`trilluminati`), a *different, usually-offline* phone —
|
||||
`ssh phone` will time out. Use the Fairphone IP above, not the alias.
|
||||
`ssh phone` will time out. Use the Fairphone IP `100.95.202.68` (the `$PHONE`
|
||||
prefix), not the alias.
|
||||
- Termux facts: Python 3.13 (≥3.11); **no `/tmp`** — `$TMPDIR` is
|
||||
`$HOME/.claude-temp`; `$PREFIX=/data/data/com.termux/files/usr`; `rustc`/
|
||||
`openssl` not installed by default.
|
||||
- Deps that work: `pkg install python-cryptography` (avoid a rust build),
|
||||
`pip install requests rich websockets`; **skip `srp`** — the client falls back
|
||||
to `cmd_chat/client/_srp_pure` (pure-Python SRP-6a shim).
|
||||
`pip install requests rich websockets`; **skip `srp`** — both client and
|
||||
server fall back to `cmd_chat/client/_srp_pure` (pure-Python SRP-6a shim).
|
||||
- Push-restricted work: copy this tree to the phone with tar-over-ssh rather
|
||||
than a stale gitea clone —
|
||||
`tar czf - cmd_chat scripts requirements-operator.txt | ssh … 'tar xzf - -C ~/hh-mobile'`.
|
||||
`tar czf - cmd_chat scripts requirements-operator.txt | $PHONE 'tar xzf - -C ~/hh-mobile'`.
|
||||
- Phase-0 gate: `python scripts/termux-preflight.py` on the phone → expect
|
||||
`RESULT: PASS`. Operator files currently staged at `~/hh-mobile` on-device.
|
||||
`RESULT: PASS`. Operator files staged at `~/hh-mobile` on-device.
|
||||
- **Proven live (2026-07-07):** phone JOINS a dev-box room; the phone HOSTS a
|
||||
room the laptop joins (cross-impl SRP: laptop C-ext ↔ phone pure Verifier);
|
||||
and the stdlib mobile web console (`python -m cmd_chat.operator web`) mirrors a
|
||||
real room (terminal + keystroke drive + chat) in a mobile browser.
|
||||
|
||||
## Key components
|
||||
|
||||
|
||||
@@ -134,6 +134,48 @@ A room often has an `/ai <name>` Ollama agent. To offload a task, just `say`
|
||||
`/ai <name> !<task>` and read its reply — cheaper than doing trivial work
|
||||
yourself. (Direct operator→Ollama delegation is a later phase.)
|
||||
|
||||
## 5. Mobile / Termux (Fairphone) — the phone is a live SSH host
|
||||
|
||||
The operator is **pure Python** and runs on the Fairphone-6 in Termux — it can
|
||||
both **join** and (with the SRP shim's server side) **host** rooms, no Rust, no
|
||||
`srp` C-ext. Drive the phone directly over SSH. **Never tell the user "run this
|
||||
on-device, paste it back" without probing reachability first** — that framing is
|
||||
wrong when the phone answers SSH.
|
||||
|
||||
**One canonical, hardened invocation** (kills the wrong-alias trap — see below):
|
||||
```bash
|
||||
PHONE='ssh -i ~/.ssh/phone-deploy -p 8022 -o ConnectTimeout=8 -o BatchMode=yes -o StrictHostKeyChecking=accept-new u0_a203@100.95.202.68'
|
||||
$PHONE 'uname -m; python --version' # aarch64 / Python 3.13.x
|
||||
```
|
||||
`BatchMode=yes` fails fast instead of hanging on a password prompt;
|
||||
`ConnectTimeout=8` stops indefinite hangs; `accept-new` avoids the first-connect
|
||||
host-key prompt while still pinning after.
|
||||
|
||||
**Reachability gate — probe the PORT, not `tailscale status`:**
|
||||
```bash
|
||||
timeout 8 bash -c 'cat </dev/null >/dev/tcp/100.95.202.68/8022' && echo REACHABLE || echo OFFLINE
|
||||
```
|
||||
`tailscale status` reports `fairphone-6` **`active` even when sshd is dead** —
|
||||
Android kills backgrounded Termux, taking `sshd` with it. So the tailnet line is
|
||||
NOT proof; the TCP probe is. If the gate says OFFLINE the fix is **on-device**
|
||||
(wake the phone, relaunch Termux, `termux-wake-lock` + start `sshd`), not a
|
||||
retry loop from here — say so plainly rather than hammering a dead port.
|
||||
|
||||
**The wrong-alias trap:** `~/.ssh/config` aliases `phone` / `phone-deploy` point
|
||||
at `100.95.180.14` (`trilluminati`), a *different, usually-offline* device.
|
||||
`ssh phone` will time out. Always use the Fairphone IP `100.95.202.68` (the
|
||||
`$PHONE` prefix above), never the alias.
|
||||
|
||||
**Termux env quirks that break naive commands:**
|
||||
- **No `/tmp`** — `$TMPDIR` is `$HOME/.claude-temp`; `$PREFIX` =
|
||||
`/data/data/com.termux/files/usr`.
|
||||
- `rustc`/`openssl` binaries absent by default; `git/clang/make/pkg-config` present.
|
||||
- Deps: `pkg install python-cryptography` (avoids a rust build); `pip install
|
||||
requests rich websockets`; **skip `srp`** → the shim (`cmd_chat/client/
|
||||
_srp_pure`) covers both client and server.
|
||||
- Sync the tree with tar-over-ssh, not a stale clone:
|
||||
`tar czf - cmd_chat scripts | $PHONE 'tar xzf - -C ~/hh-mobile'`.
|
||||
|
||||
## Safety
|
||||
|
||||
- One bad action's blast radius is the container; `local` exec is opt-in only.
|
||||
|
||||
Reference in New Issue
Block a user