Wire the on-box (Claude-API-less) path decided with the operator: EXTRACT_BACKEND=ocr sends each screenshot to the on-box mrnumber-ocr service (raw text, no per-shot structuring); build_rating_profile uses an OpenAI-compatible LLM on a DO GPU droplet (RATING_LLM_URL) which extracts the reports from the raw OCR text AND produces the multi-axis verdict. Reports are folded back into the history so the people-signal + counts + safety flags reflect them; safety detection also scans the raw OCR lines so a LE term forces cop_flag even before structuring. vision/Claude stays the plum-dev default. +5 tests incl. full OCR→GPU→cop_flag flow. 33/33. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| client | ||
| deploy | ||
| docs | ||
| mcp | ||
| service | ||
| .gitignore | ||
| .infra.yaml | ||
| app.manifest.yaml | ||
| CLAUDE.md | ||
| README.md | ||
@mr-number
A sister feeder app of Prospector in the cocotte-tech (@ct) ecosystem: automated
Mr. Number caller screening as a number-reputation source.
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
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.
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.
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
│ HTTP (service token, WG mesh) │ · /data on volume redroidmrnumberdata
▼ └─────────────────────────────────┘
cocotte people service (persons DB) · POST /internal/people/signals (lime:3061, mesh-only)
signalType=screening_mrnumber, keyed by phone → consumed by Prospector
Device paths
- Cloud (primary): the redroid droplet
lilith-store-redroid(45.55.191.82), reached over adb. Persistent/datavolume 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:8001to localhost). - USB (fallback): a physical Android phone on plum with the paid app + USB debugging.
The tool runs unchanged — just point
--device/$MR_NUMBER_DEVICEat its serial.
Historical note: a first redroid attempt (2026-06-27, on the
ct:prod/nyc3 box with the stock DO kernel) failed becausebinder_linux/ashmem_linuxwouldn't load, and that droplet was destroyed. The post-mortem is preserved underdocs/archive/. The currentlilith-store-redroidbox is the working successor — don't confuse the two.
Coupling (bidirectional, HTTP only — no Quinn)
Prospector is the consumer; this app is the number-reputation feeder.
- Record (app → people service).
mr_lookup.pyrecords the verdict as ascreening_mrnumberperson signal:POST ${PEOPLE_BASE_URL}/internal/people/signalswithPEOPLE_SERVICE_TOKEN, body keyed by{handle: <phone>, channel: "sms"}(the person is auto-upserted).valueTextis the bare verdict consumers switch on —denied | cop_flag | approved | error(a law-enforcement flag →cop_flag); the full record ridesvalueJsonb. - Consume (Prospector). Prospector's
MrNumberClient.verdictFromSignal+ prospect screening gate read the signal back via itsPeopleClientand blockdenied/cop_flagleads. Lives in@applications/prospector, not here. - Trigger (Prospector → app). Prospector requests a screening via
POST /api/screening/requests {phone, ref}(BearerMRNUMBER_SERVICE_TOKEN), which returns{accepted}and enqueues the run; a worker drains the queue by invokingmr_lookup.py, and the verdict lands asynchronously as the signal in (1). This trigger service ships here (see "Trigger service" — in progress).
Environment
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)"
export MR_NUMBER_DEVICE="45.55.191.82:5555" # redroid; or a USB serial like R58N123ABC
# optional: export CLAUDE_CODE_BATCH_SDK_PATH="$HOME/Code/@quinn/@applications/ml/@packages/@py/claude-code-batch-sdk/src"
Usage
# plum needs adb (brew install android-platform-tools)
adb connect 45.55.191.82:5555 # redroid; skip for a USB phone
cd client
python3 mr_lookup.py --phone "+1 555 123 4567" [--ref prospect-123] [--dry-run] [--device 45.55.191.82:5555]
python3 -m unittest mr_lookup_test -v # host-free unit tests
--ref— optional requester correlation id (carried into the signal'ssourceHandle).--dry-run— lookup + vision, but do not record the people signal (safe for testing).--json— emit one JSON result object on stdout (used by the MCP).
Console (sign-in / calibration)
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)
MCP (coworker-agent / Claude Desktop)
cd mcp && bun run start # stdio server; exposes mr_number_lookup
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.
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:
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:
# MRNUMBER_BASE_URL=http://10.9.0.6:8787 (box WG mesh leg)
Local dev (plum):
cd service && ./run # loads ~/.config/cocotte-secrets/*; binds 0.0.0.0:8787
bun test && bun run typecheck # host-free
| 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} |
Layout
client/ plum-side lookup + console tray
mr_lookup.py adb drive → screenshot → vision → decide → record people signal
mr_lookup_test.py host-free unit tests (mock adb/vision/network)
console-tray/ macOS menu-bar SSH-tunnel console to the redroid box
service/ bun trigger service — POST /api/screening/requests → durable
SQLite queue → serial worker driving mr_lookup.py (Prospector calls this)
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.