# SMS Bridge — self-hosted BETA how-to

**Snapshot:** **1.1.0-beta** (see `VERSION`). Not 1.2.0.

**Status:** experimental personal/self-host beta packaging.  
**Not** a public product launch. **Not** security-reviewed. **Not** available on Google Play / app stores.  
**Licensing:** decision pending — this tree does **not** grant open-source, commercial, or redistribution rights; third-party Android/Python/OS components keep their own licenses. See `LICENSE-DECISION-PENDING.txt`.

This guide packages what was recoverable without touching a live phone bridge, live reverse proxy, or personal message data.

## What this is

A **single-owner, self-hosted** relay:

1. **Android companion app** (sideloaded) on one phone you control  
2. **Your** Linux Python bridge + SQLite on a host you control  
3. **HTTPS** front door (e.g. Caddy) on a **dedicated hostname** you control  
4. **Local stdio MCP adapter** on the assistant box, holding an access key in a protected file  
5. Assistant connector registration (account-local; may apply to multiple bots under the same owner — confirm in UI)

There is **no** multi-tenant hosted service in this design. Do **not** share one MCP access key or one device pairing across unrelated people.

## Verified vs not (honest)

| Capability | Evidence in this **1.1.0** snapshot |
|---|---|
| Individual SMS send through phone | Verified once on operator's Samsung (self-send test) |
| SMS + MMS **text** read, including **group MMS** participants / `is_group` / protocols | Verified on **one** Samsung after 1.1.0 (not claimed universal) |
| Feature scope | SMS + MMS text only; single-recipient SMS send; attachment **types** may list, bytes not retrieved; other chat transports out of scope |
| MMS image/attachment retrieval | **Not in 1.1.0** (tracked separately as 1.2.0 work) |
| Auto-start after boot / update / app-open | **Best-effort only** in 1.1.0 (`BootReceiver`); after APK updates reopen + **Start bridge**. Improved auto-start is 1.2.0 work — not claimed here |
| HTTP→HTTPS redirect quirks | Seen on one personal deployment; **not** a generic claim |
| Security review | Informal threat model only — **not** a formal audit |

Unit tests in `android-companion/` cover pure MsgLogic merge/address helpers only.

## Architecture

```
[Assistant / Grok Bot]
    |  stdio MCP (local adapter + token file)
    v
[Your HTTPS hostname]  --Caddy/TLS-->  [127.0.0.1 Python bridge + SQLite]
    ^                                         |
    |                         long-poll /device/poll + /device/result
    +---------------- [Android companion app on phone] ------------------+
```

- Transport encryption: TLS to your hostname. **Not** end-to-end encryption of SMS bodies between phone and assistant.  
- Device token: stored on phone encrypted with Android Keystore AES-GCM.  
- Send path: assistant must obtain **explicit user approval** before `send_sms` with `confirm=true` (product policy + app check).  
- Dedup: phone journals command IDs before radio send; never blind-retries in-flight sends.

## Prerequisites

- Android phone with SMS (tested path: Samsung sideload). minSdk 26 / target 35 in this tree.  
- Linux host with Python 3, ability to run a local service + SQLite.  
- Public DNS name dedicated to this bridge + reverse proxy that terminates TLS.  
- Ability to sideload APKs ("Install unknown apps") and, on Samsung/One UI when SMS is blocked, **Allow restricted settings**.  
- Assistant environment that can run a **custom MCP command** (python3 + adapter script + env).

## Bundle layout

```
HOW_TO_SELF_HOST_BETA.md          ← this file
LICENSE-DECISION-PENDING.txt
THIRD_PARTY_NOTICES.md
MANIFEST.sha256
android-companion/                ← sanitized source (package org.smsbridge.companion)
binaries/SMSBridge-Companion-1.1.0-beta.apk  ← optional prebuilt companion
server/                           ← sanitized Python bridge, pair/revoke, tests, unit + Caddy templates
mcp-adapter/sms-bridge-mcp-stdio.py
docs/PHONE_API_CONTRACT.md
docs/THREAT_MODEL.md
docs/ROLLBACK.md
scripts/bootstrap-mcp-token.sh
scripts/run-isolated-server-tests.sh
scripts/security-scan.sh
EXCLUSIONS.md
```

**Excluded on purpose:** personal APK signed for `a private operator package id`, keystores, token files, SQLite DBs, message logs, contacts, live Caddy with real host/IP, build caches.

## 1) Build the companion APK (recommended)

Personal prebuilt APKs defaulted to a real hostname and a personal package/signature — **do not redistribute those**.

From `android-companion/`:

```bash
./run-unit-tests.sh
./build.sh
# → out/SMSBridge-Companion-1.1.0-beta.apk
```

`build.sh` creates a **new** keystore under `~/.secrets/smsbridge-beta-companion/` by default (outside the ZIP). Keep the `.jks` / pass private.

Default server URL in the companion is **empty** — the operator types `https://their-host` before pairing.

### Alternative: source-only

If you cannot build here, ship source + these instructions; recipients build with Android build-tools 35 + JDK 17.

## 2) Server + HTTPS

Sources are in `server/` (Python 3 stdlib only).

```bash
mkdir -p ~/sms-bridge && cp server/server.py server/pair.sh server/revoke.sh ~/sms-bridge/
chmod 755 ~/sms-bridge/*.sh
export SMSB_HOME=~/sms-bridge
# Generate MCP token + hash (writes mode-0600 files; do not paste token into chat)
./scripts/bootstrap-mcp-token.sh
# Optional: copy hash only to the bridge host if token file lives elsewhere
mkdir -p ~/.config/systemd/user
cp server/systemd/sms-bridge.service ~/.config/systemd/user/
# Edit the unit: set SMSB_PROXY to your reverse-proxy peer IP when proxied (often 127.0.0.1)
systemctl --user daemon-reload
systemctl --user enable --now sms-bridge.service
```

Reverse proxy: start from `server/caddy/Caddyfile.example` with a **dedicated** hostname.  
`SMSB_PROXY` defaults to **empty (fail-closed)** — X-Forwarded-For is ignored until you set it.  
If you omit `SMSB_PROXY` behind a proxy on a private IP, LAN test routes (`/api/health`, `/`) can leak on the public hostname — set it.

Public paths only: `/status`, `/device/*` (device bearer), `/mcp` (MCP bearer).  
Localhost admin: `pair.sh` / `revoke.sh` → `/admin/*`.  
Phone API: `docs/PHONE_API_CONTRACT.md`.

Isolated synthetic tests (no phone): `./scripts/run-isolated-server-tests.sh`.

## 3) Install on Android (sideload)

1. Transfer the **companion** APK to the phone; allow install from that source.  
2. Open **App info** for SMS Bridge.  
3. Grant SMS / Phone / Notifications as prompted.  
4. If allowing SMS says the app was denied and suggests restricted access:  
   - Leave Permissions, return to the **main App info** screen  
   - Use the **⋮ / three-dot menu** → **Allow restricted settings**  
   - Authenticate, then grant SMS again  
5. Battery → **Unrestricted** (OEM names vary).  
6. Enter your **HTTPS** base URL (no path) and the one-time pairing code from the server.  
7. Tap **Pair this phone**, then **Start bridge**.  
8. Confirm the persistent foreground notification ("SMS Bridge running").

**After every APK update:** reopen the app, confirm version, tap **Start bridge** again (service may not auto-resume).

## 4) Register the MCP connector

1. Place `mcp-adapter/sms-bridge-mcp-stdio.py` on the assistant machine.  
2. Write the MCP token to a protected file (e.g. `chmod 600`).  
3. Register a custom MCP command approximately like:

```text
command: python3
args:    ["/absolute/path/sms-bridge-mcp-stdio.py"]
env:
  SMSB_MCP_URL=https://smsbridge.example/mcp
  SMSB_MCP_TOKEN_FILE=/absolute/path/mcp-access.token
```

4. Confirm registration in the product UI. Connector config may be **account-local** (shared across that owner's bots) — verify before enabling broadly.  
5. Wait until tools finish loading; then call health/status first.

**Never** paste device tokens, pairing codes, or MCP keys into chat.

## 5) Smoke test (with consent)

1. Health / status — phone online, no send.  
2. List threads — expect SMS and (when present) MMS; group threads show `is_group` / multiple participants.  
3. Read one thread the owner named — use completion/status polling if a command was queued; do **not** blindly re-issue duplicates.  
4. Send only after **explicit** owner approval of recipient + exact body.

Verification codes and OTPs: keep out of chat and durable logs whenever possible.

## 6) Operations notes

- One phone account per self-hosted bridge is the intended model.  
- Rotate MCP keys and revoke devices on loss/theft.  
- Offline phone / airplane mode / process death → assistant commands wait or fail; sends left mid-flight become `unknown`, not retried.  
- Reboot/update: see 1.1.0 auto-start row above (manual Start still expected).

## 7) Rollback / uninstall

See `docs/ROLLBACK.md`.

## Threat model (summary)

See `docs/THREAT_MODEL.md`. Short version: anyone who can call `/mcp` with your key, or control the phone app / server disk, can read SMS/MMS text the phone can see and can queue sends the product policy allows. TLS protects the path; it is **not** E2EE. Sideloaded SMS apps are sensitive — treat the APK signing key as critical.

## Remaining gaps (honest)

1. Operator **license choice** still pending (`LICENSE-DECISION-PENDING.txt`).  
2. Broader device testing beyond one Samsung.  
3. Formal security review — not done.  
4. Distribution decision (direct APK only vs any store attempt — Play SMS policy is hostile to this use case).  
5. Not a multi-tenant hosted product; each owner self-hosts one phone account.
