2026-06-28 09:06:51 -04:00
# @mr-number
2026-06-29 13:44:25 -04:00
A **sister feeder app of Prospector** in the cocotte-tech (`@ct` ) ecosystem: automated
**Mr. Number** caller screening as a number-reputation source.
2026-06-28 09:06:51 -04:00
It drives the paid Mr. Number Android app (`com.mrnumber.blocker` , by Hiya — no public
API), looks up a phone number, screenshots the crowdsourced community reports, extracts
them with the project's Claude vision SDK, decides a screening verdict, and records it
2026-06-29 13:44:25 -04:00
as a ** `screening_mrnumber` person signal** in the cocotte **people service** (persons
DB), keyed by the number. Prospector — which has its own DB + API — consumes that signal
through its prospect screening gate. **There is no old-Quinn coupling.**
2026-06-28 09:06:51 -04:00
2026-06-29 13:44:25 -04:00
Like **mac-sync** , this is its own repo and couples to the rest of the ecosystem only
over HTTP — it does **not** import platform code or share a DB/registry/port.
2026-06-28 09:06:51 -04:00
## Topology (two tiers, like mac-sync)
```
┌─ plum (this Mac) ─────────────────────────┐ ┌─ DO redroid droplet ────────────┐
│ client/mr_lookup.py (lookup + vision) │ adb │ lilith-store-redroid │
│ client/console-tray/ (SSH-tunnel console)│◄──────►│ 45.55.191.82 │
│ mcp/ (stdio MCP for │ :5555 │ · redroid Android (Mr. Number) │
│ coworker / Desktop) │ │ · ws-scrcpy :8000 │
└───────────────┬───────────────────────────┘ │ · cloud/adb-keyboard server :8001
2026-06-29 13:44:25 -04:00
│ HTTP (service token, WG mesh) │ · /data on volume redroidmrnumberdata
2026-06-28 09:06:51 -04:00
▼ └─────────────────────────────────┘
2026-06-29 13:44:25 -04:00
cocotte people service (persons DB) · POST /internal/people/signals (lime:3061, mesh-only)
signalType=screening_mrnumber, keyed by phone → consumed by Prospector
2026-06-28 09:06:51 -04:00
```
### Device paths
- **Cloud (primary):** the redroid droplet `lilith-store-redroid` (`45.55.191.82` ),
reached over adb. Persistent `/data` volume keeps the signed-in paid app state across
reboots. Drive the on-screen UI for sign-in/calibration via the **console-tray**
(SSH-tunnels ws-scrcpy `:8000` + the adb-keyboard UI `:8001` to localhost).
- **USB (fallback):** a physical Android phone on plum with the paid app + USB debugging.
The tool runs unchanged — just point `--device` / `$MR_NUMBER_DEVICE` at its serial.
> Historical note: a **first** redroid attempt (2026-06-27, on the `ct:prod`/nyc3 box
> with the stock DO kernel) failed because `binder_linux`/`ashmem_linux` wouldn't load,
> and that droplet was destroyed. The post-mortem is preserved under `docs/archive/`.
> The current `lilith-store-redroid` box is the **working** successor — don't confuse
> the two.
2026-06-29 13:44:25 -04:00
## Coupling (bidirectional, HTTP only — no Quinn)
Prospector is the consumer; this app is the number-reputation feeder.
1. **Record (app → people service).** `mr_lookup.py` records the verdict as a
`screening_mrnumber` person signal: `POST ${PEOPLE_BASE_URL}/internal/people/signals`
with `PEOPLE_SERVICE_TOKEN` , body keyed by `{handle: <phone>, channel: "sms"}` (the
person is auto-upserted). `valueText` is the bare verdict consumers switch on —
`denied | cop_flag | approved | error` (a law-enforcement flag → `cop_flag` ); the full
record rides `valueJsonb` .
2. **Consume (Prospector).** Prospector's `MrNumberClient.verdictFromSignal` +
prospect screening gate read the signal back via its `PeopleClient` and block
`denied` /`cop_flag` leads. Lives in `@applications/prospector` , not here.
3. **Trigger (Prospector → app).** Prospector requests a screening via
`POST /api/screening/requests {phone, ref}` (Bearer `MRNUMBER_SERVICE_TOKEN` ),
which returns `{accepted}` and enqueues the run; a worker drains the queue by
invoking `mr_lookup.py` , and the verdict lands asynchronously as the signal in (1).
**This trigger service ships here** (see "Trigger service" — in progress).
2026-06-28 09:06:51 -04:00
## Environment
```bash
2026-06-29 13:44:25 -04:00
export PEOPLE_BASE_URL="http://10.9.0.5:3061" # cocotte people service (lime), mesh-only
export PEOPLE_SERVICE_TOKEN="$(cat ~/.config/cocotte-secrets/people-service.token)"
2026-06-28 09:06:51 -04:00
export MR_NUMBER_DEVICE="45.55.191.82:5555" # redroid; or a USB serial like R58N123ABC
2026-06-29 13:44:25 -04:00
# optional: export CLAUDE_CODE_BATCH_SDK_PATH="$HOME/Code/@quinn/@applications/ml/@packages/@py/claude-code-batch-sdk/src"
2026-06-28 09:06:51 -04:00
```
## Usage
```bash
# plum needs adb (brew install android-platform-tools)
adb connect 45.55.191.82:5555 # redroid; skip for a USB phone
cd client
2026-06-29 13:44:25 -04:00
python3 mr_lookup.py --phone "+1 555 123 4567" [--ref prospect-123] [--dry-run] [--device 45.55.191.82:5555]
2026-06-28 09:06:51 -04:00
python3 -m unittest mr_lookup_test -v # host-free unit tests
```
2026-06-29 13:44:25 -04:00
- `--ref` — optional requester correlation id (carried into the signal's `sourceHandle` ).
- `--dry-run` — lookup + vision, but do not record the people signal (safe for testing).
2026-06-28 09:06:51 -04:00
- `--json` — emit one JSON result object on stdout (used by the MCP).
### Console (sign-in / calibration)
```bash
2026-06-28 10:55:59 -04:00
client/console-tray/run.sh # menu-bar tray (☎️ icon); Connect → Open Console (http://localhost:8001/ui?app=mr-number — now shows clear ☎️ Mr. Number header in the webui)
2026-06-28 09:06:51 -04:00
```
### MCP (coworker-agent / Claude Desktop)
```bash
cd mcp & & bun run start # stdio server; exposes mr_number_lookup
```
2026-06-29 13:57:54 -04:00
### Trigger service (Prospector → app)
The inbound HTTP surface Prospector calls. Durable SQLite queue + a single serial
worker (one Android box) that drains by invoking `mr_lookup.py` ; the verdict lands as
the people-service signal.
feat(deploy): trigger service deploys to the redroid box (systemd), not plum (launchd)
Per the corrected .infra.yaml, the trigger service is a PROD unit that runs ON the
redroid box (next to the Android container → local adb), with plum as dev-only. Replace
the (wrong) macOS LaunchAgent with the box-native pattern, mirroring @redroid's
deploy-droplet.sh / mrnumber-ocr.service:
- deploy/mr-number-service.service: systemd unit (multi-user.target, EnvironmentFile for
tokens, MR_NUMBER_DEVICE=localhost:5555, __BUN__ resolved at deploy time).
- deploy/deploy-service.sh: prereq-checks the box (bun/python3/redroid_client/adb), scp's
service+client to /opt/mr-number-service, installs the unit, seeds a 0600 env template,
enables only once tokens are filled (no crashloop). Does NOT mint tokens.
- service/run: drop launchd installer; plain dev launcher (loads cocotte-secrets).
- install.sh: plum = dev setup only; points prod at deploy-service.sh.
- CLAUDE.md/README: box-deploy ownership split (@redroid owns the box; we own our unit).
Syntax-checked; box SSH (:22) unreachable from this env so not yet run against the box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:19:40 -04:00
**Prod: runs ON the redroid box** (systemd unit `mr-number-service` , next to the Android
container so adb is local), bound `0.0.0.0:8787` . Deploy from here:
2026-06-29 13:57:54 -04:00
```bash
feat(deploy): trigger service deploys to the redroid box (systemd), not plum (launchd)
Per the corrected .infra.yaml, the trigger service is a PROD unit that runs ON the
redroid box (next to the Android container → local adb), with plum as dev-only. Replace
the (wrong) macOS LaunchAgent with the box-native pattern, mirroring @redroid's
deploy-droplet.sh / mrnumber-ocr.service:
- deploy/mr-number-service.service: systemd unit (multi-user.target, EnvironmentFile for
tokens, MR_NUMBER_DEVICE=localhost:5555, __BUN__ resolved at deploy time).
- deploy/deploy-service.sh: prereq-checks the box (bun/python3/redroid_client/adb), scp's
service+client to /opt/mr-number-service, installs the unit, seeds a 0600 env template,
enables only once tokens are filled (no crashloop). Does NOT mint tokens.
- service/run: drop launchd installer; plain dev launcher (loads cocotte-secrets).
- install.sh: plum = dev setup only; points prod at deploy-service.sh.
- CLAUDE.md/README: box-deploy ownership split (@redroid owns the box; we own our unit).
Syntax-checked; box SSH (:22) unreachable from this env so not yet run against the box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:19:40 -04:00
deploy/deploy-service.sh # scp service+client to /opt, install unit, restart
# then on the box fill /etc/mr-number-service.env (tokens), and point Prospector at:
2026-06-29 17:31:54 -04:00
# MRNUMBER_BASE_URL=http://10.9.0.6:8787 (box WG mesh leg)
feat(deploy): trigger service deploys to the redroid box (systemd), not plum (launchd)
Per the corrected .infra.yaml, the trigger service is a PROD unit that runs ON the
redroid box (next to the Android container → local adb), with plum as dev-only. Replace
the (wrong) macOS LaunchAgent with the box-native pattern, mirroring @redroid's
deploy-droplet.sh / mrnumber-ocr.service:
- deploy/mr-number-service.service: systemd unit (multi-user.target, EnvironmentFile for
tokens, MR_NUMBER_DEVICE=localhost:5555, __BUN__ resolved at deploy time).
- deploy/deploy-service.sh: prereq-checks the box (bun/python3/redroid_client/adb), scp's
service+client to /opt/mr-number-service, installs the unit, seeds a 0600 env template,
enables only once tokens are filled (no crashloop). Does NOT mint tokens.
- service/run: drop launchd installer; plain dev launcher (loads cocotte-secrets).
- install.sh: plum = dev setup only; points prod at deploy-service.sh.
- CLAUDE.md/README: box-deploy ownership split (@redroid owns the box; we own our unit).
Syntax-checked; box SSH (:22) unreachable from this env so not yet run against the box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:19:40 -04:00
```
**Local dev (plum):**
```bash
cd service & & ./run # loads ~/.config/cocotte-secrets/*; binds 0.0.0.0:8787
bun test & & bun run typecheck # host-free
2026-06-29 13:57:54 -04:00
```
| Method + path | Auth | Body / result |
| --- | --- | --- |
| `POST /api/screening/requests` | Bearer | `{phone, ref?}` → `202 {accepted, id}` ; verdict lands async as the signal |
| `GET /api/screening/requests/:id` | Bearer | the queue row (`status` , `verdict` ) |
| `GET /health` | none | `{ok, pending}` |
2026-06-28 09:06:51 -04:00
## Layout
```
client/ plum-side lookup + console tray
2026-06-29 13:57:54 -04:00
mr_lookup.py adb drive → screenshot → vision → decide → record people signal
2026-06-28 09:06:51 -04:00
mr_lookup_test.py host-free unit tests (mock adb/vision/network)
console-tray/ macOS menu-bar SSH-tunnel console to the redroid box
2026-06-29 13:57:54 -04:00
service/ bun trigger service — POST /api/screening/requests → durable
SQLite queue → serial worker driving mr_lookup.py (Prospector calls this)
2026-06-28 09:06:51 -04:00
mcp/ bun stdio MCP wrapping mr_lookup.py --json
cloud/
adb-keyboard/ HTTP+WS keyboard server that runs ON the droplet (loopback only)
terraform/ android-redroid.tf.reference — read-only copy; canonical IaC is in uvlava
deploy/ install.sh (plum) + deploy-droplet.sh (push adb-keyboard to the box)
docs/archive/ first-attempt (failed) redroid + web-console handoffs, for history
```
## Infrastructure ownership
The redroid droplet itself is **not** provisioned from this repo. Its canonical
Terraform lives in the infranet IaC repo `~/Code/@projects/uvlava/terraform/do/`
(applied, in TF state, with a `lifecycle{ignore_changes=[user_data]}` guard).
`cloud/terraform/android-redroid.tf.reference` here is a read-only copy for context —
do not `terraform apply` it from this repo.