Open-source project (Apache-2.0) that lets any AI agent (Claude Code, OpenClaw, n8n, scripts) call the user's phone over VoIP and message it, and lets the user call the agent back. The phone must ring like a real call when locked. No PSTN, no numbers, no Twilio. F1 scope: one user, one agent, Android only, self-hosted, no accounts.
- The user writes in Spanish; answer in Spanish. Code, comments, docs and README in English.
- Work milestone by milestone (M1→M5). At the end of each milestone: stop, explain how to test it, and wait for the user's OK before starting the next.
- Ask before deviating from the design in
docs/architecture.md/docs/protocol.md. Record accepted deviations there. - The protocol contract is
protocol/relay.openapi.yaml(single source of truth). Change it first, then the implementations, and keepprotocol/examples/fixtures valid. - Develop on
main(the default branch; renamed fromclaude/agentring-voip-calls-l4ei3s). Commit with clear messages; don't open PRs unless asked. Onlymainpushes replace theandroid-previewAPK.
| Milestone | Scope | Status |
|---|---|---|
| M1 | relay + pairing + messages + push | Done; verified on the user's Pixel 6 (Android 16), FCM incl. forced Doze (~1.5 s). |
| M2 | phone rings locked / Doze in < 5 s with native call UI; audio (echo/tone is fine) | In progress: works on the Pixel 6 (locked + Doze → call screen in 1.6 s, in the room ~0.5 s after answer). Group calls + "join the call" while active (POST /v1/calls/{id}/join, Join notification/banner; not yet tested on a real phone). TODO: LiveKit in deploy compose, user's listening test, OK. |
| M3 | Gemini Live conversation (< 1 s turns), OpenAI Realtime optional; consult_agent with backend: command |
In progress (merged in #2): generic agentring.voice library (VoiceAgent, pluggable RealtimeBackend, Python/MCP tools, hooks), call --ai, serve --ai answering the phone; live e2e tests pass (docs/voice.md). Providers: Gemini Live (default), OpenAI Realtime, Grok Voice, Phonic, and local (experimental: any OpenAI-Realtime-compatible server at AGENTRING_VOICE_URL, e.g. HF speech-to-speech + Ministral 3 3B on llama.cpp; no model bundled) (--provider / AGENTRING_VOICE_PROVIDER, issue #12); agentring voice bench runs the blog's 4-prompt test against any provider; Prompts.before_tools (say something before a tool) took Gemini's first word on a slow tool from 5.1 s to 1.5 s; live e2e passes with Gemini, Grok and Phonic, OpenAI only against fake servers so far. consult_agent + agents.yaml done (docs/agents.md; slow answers come as a note, or a message after the call). TODO: OpenAI live run, transcripts in the store, real-phone test. |
| M4 | call the agent from the app; missed/declined fallbacks | In progress: app side done on branch m4-call-agent ("Call agent" → POST /v1/calls, joins the room at once, "Calling…" until call.accept / agent in the room / poll, cancel while ringing, agent offline / busy / declined / missed messages; not yet tested on a real phone). Agent side: serve --ai answers. Missed → msg.new fallback done for AI calls (call --ai, MCP call). Android Auto (issue #20): "Call " car screen (CarAppService, CALLING category) in the manifest-only :auto module, included in debug builds and in release only with -PagentringAndroidAuto=true (GitHub APK yes, Play production no: Play rejects car calling apps outside internal/closed tracks); Telecom mute from the car followed by CallManager; docs/android-auto.md; checked on the Desktop Head Unit with the Pixel 6 locked (call from the car, incoming call answered in the car, mute, hang up; the FGS starts from the car session as "BTOP"), not yet in a real car. |
| M5 | MCP server, skill, examples, docs, < 15 min quickstart with docker compose up |
In progress: agentring mcp (stdio, no SDK), skills/agentring, examples (claude-code, n8n), LiveKit + optional daemon in the basic compose, release workflow (signed APK, PyPI, GHCR; docs/release.md), SECURITY/CONTRIBUTING, OEM guide. OpenClaw example + per-harness recipes (docs/agents.md: Claude Code, Codex, opencode, OpenClaw, Hermes, HTTP agents). `agentring connect openclaw |
| Cloud | hosted agentring (docs/cloud.md) | In progress (2026-11-01, user asked for all of it at once): relay hub mode (relay/src/hub/, tenants under /t/{id}, admin API, plan limits → 402 quota_exceeded), agentring mcp-http (remote MCP, Streamable HTTP, hosted voice), app link to the cloud (-PagentringCloudUrl). Control plane (landing, waitlist, magic-link sign-in, dashboard, Stripe) + Coolify compose in the private repo agentring-cloud. Deployed 2026-11-01 at https://agentring.io (Coolify app kkrlxmaht8kcihopnce8xz8i on balrog; prod smoke test incl. a real Grok voice call passed). App targets API 36; Play release steps in docs/release.md. App live on Google Play since 2026-11-04 (dev.agentring.app, TONVI TECH SL, Play App Signing). TODO: Stripe keys in Coolify, OAuth for claude.ai connectors, legal review of privacy/terms. |
Agreed direction for M3: keep agentring agnostic with two plug points — the
voice backend (Gemini Live, OpenAI Realtime, Grok Voice, Phonic; later a local
OpenAI-Realtime-compatible server such as Hugging Face speech-to-speech via
OpenAIRealtime(url=…)) and the tools (any local MCP
server, e.g. jefe-mcp on :8787, plus the built-in consult_agent). Over the
phone, only an allowlist of tools. The user's own agent in production
(tonvi-agentes on Coolify) talks on calls with Grok Voice; its
VOICE_PROVIDER=openai is only the Telegram voice-note transcription.
Dependency policy (user asked for care after recent supply-chain attacks):
prefer releases at least ~2 weeks old, pin exact versions, check the lockfiles
against OSV before pushing. Apply age limits per package (uv lock --exclude-newer-package), never globally: a global --exclude-newer once
downgraded certifi to a version with compromised CA roots.
server.json MCP Registry entry for the hosted MCP server (docs/release.md#directories)
plugins/ agentring/ = Grok Build plugin (hosted MCP + a copy of skills/agentring; a test keeps it equal)
protocol/ relay.openapi.yaml (REST + event schemas), examples/ (event fixtures, push signature vector)
relay/ TypeScript, Fastify 5, node:sqlite (no native deps), jose; FCM HTTP v1 without firebase-admin
daemon/ Python ≥3.11 + uv; `agentring` CLI (typer), local SQLite mirror synced from the relay
app-android/ :core = pure Kotlin/JVM (protocol models, PushVerifier, RelayApi, RelaySocket)
:app = Compose/Material 3 app, package dev.agentring.app, minSdk 26, targetSdk 35
deploy/ docker-compose.yml (caddy + relay + LiveKit), docker-compose.local.yml (own computer, LAN/Tailscale, no TLS),
docker-compose.proxy.yml (behind an existing proxy), Caddyfile, .env.example, .env.local.example
scripts/ ci-emulator-e2e.sh, device-test.sh
docs/ architecture.md, protocol.md, setup.md, firebase.md
.github/ workflows/ci.yml (relay+daemon tests, compose smoke), workflows/android.yml (APK + emulator e2e + release)
Release APKs are signed with the release key (fingerprint in SECURITY.md); android-preview stays a debug-key test build and does not install over a release APK (uninstall first).
cd relay && npm ci && npm run typecheck && npm test && npm run build # 35 tests, Node >= 22.13
cd daemon && uv run --extra voice pytest -q # needs relay/dist (e2e against the real relay)
cd app-android && ./gradlew :core:test :app:assembleDebug
cd app-android && ./gradlew -PcoreOnly :core:test # when Google's Maven is unreachableCI (.github/workflows/android.yml) builds the APK, starts a relay, boots an
API 34 emulator, runs M1EndToEndTest (pair via UI → WS message → read receipt
→ reply in app → signed push → notification inline reply), checks
agentring replies, uploads screenshots, and replaces the android-preview
pre-release with agentring.apk.
Local real-phone test: scripts/device-test.sh [apk] or --instrumented.
- Commands are REST; events flow over a resumable WebSocket (
/v1/ws?after_seq=) from a persisted event log with a globalseq; FCM carries only what must wake the phone. - LiveKit tokens are not in
call.invite; the callee gets them fromPOST /v1/calls/{id}/accept(or/join), each phone its own. The caller gets its own in thePOST /v1/callsresponse. - FCM data payloads are signed:
sig = HMAC-SHA256(push_secret, event_json)plusrelay_id; the app rejects anything else. - Firebase is configured at runtime: the relay reads the user's
google-services.jsonand returnspush_configin the pair response, so one prebuilt APK works with anyone's Firebase project. - Extra call state
canceledand informational eventcall.ringing; one call at a time (409 busy); group calls: every phone rings, any may answer while it rings, and any paired phone not on an active call may join it later (POST /v1/calls/{id}/join;call.acceptis pushed at normal priority so the other phones can show "Join"). Targeted calls: the agent may passdevice_idsto ring only those phones; the call is then private to them (the relay filters its events, pushes and REST views for every other phone). - Auth: daemon uses an
ak_…API key (generated on first relay start, stored inDATA_DIR/api_key); devices use a long-lived HS256device_jwtchecked against the DB on every request (immediate revocation, WS closed with 4401). - The app accepts
http://relays only on private/LAN/localhost hosts (RelayUrlPolicy); everything else must be https. - Debug keystore
app-android/app/debug.keystoreis committed on purpose so CI builds install over each other. Release builds take the real key fromAGENTRING_KEYSTORE_*(docs/release.md) and fall back to the debug key locally. - M3:
consult_agentcommand backend must take an argv list, never a shell string (the task text comes from the voice model). - M2 plan: one hostname, Caddy routes
/v1/*→ relay and/rtc*,/twirp*→ LiveKit.
- Cloud sessions (claude.ai/code) cannot reach
dl.google.com/maven.google.com, so the Android app can't be built there. Use the-PcoreOnlybuild locally and let GitHub Actions build/test the app (read logs with the GitHub MCP tools; artifacts/release assets can be fetched viagh api -H "Accept: application/octet-stream" repos/mutonby/agentring/releases/assets/<id>). A cloud session also cannot see a USB phone; for that the user runs Claude Code on their own computer. - An Android SDK was unpacked to
/opt/android-sdkin one cloud session (from thecimg/androidDocker image); it does not persist across sessions. - Docker builds inside the cloud sandbox can't reach npm; the compose stack is
verified by the
deploy-smokeCI job instead. ubuntu-latestrunners need disk freed for the emulator (already in the workflow).- Local M2 testing (see
build-local/, git-excluded): relay on :8080, LiveKit in Docker (livekit/livekit-server:v1.13.10,node_ip= the Mac's LAN IP, UDP 50000-50200) because the macOS firewall blocks the Homebrewlivekit-serverbinary; WebRTC ignores loopback candidates, soadb reversecan't carry media. The phone reaches the relay and LiveKit signalling viaadb reverse tcp:8080andtcp:7880(wireless adb works). - Espresso < 3.7 crashes on Android 16 (
InputManager.getInstance); the instrumented test needs the phone unlocked. - Core-Telecom
disconnect()only accepts LOCAL/REMOTE/MISSED/REJECTED; any other cause throws and leaves a ghost RINGING call that blocks later calls (CallException 5).TelecomBridge.platformCausemaps them.
- Relay: calls endpoints (already in the OpenAPI), ring timer (30 s →
call.missed), LiveKit room + per-call tokens (TTL 120 s, one room), LiveKit webhook, FCM push ofcall.invite(TTL = untilexpires_at) and of accept/decline/end/missed for ringing calls. - Daemon:
agentring call "reason" [--urgent] [--wait], voice worker (LiveKit Agents) joining the room — echo/tone is enough for M2. - App: self-managed
ConnectionService+PhoneAccount(MANAGE_OWN_CALLS), CallStyle notification with full-screen intent, Android 14+ FSI declaration, urgent priority → alarm stream + bypass DnD when policy access granted, LiveKit Android SDK audio, active-call screen (mute, speaker, hang up), full onboarding (notifications, mic, FSI, DnD, battery optimisation with Xiaomi/Samsung/Huawei instructions, test call). - Acceptance: phone locked, screen off, 10 min in Doze →
agentring call "test"rings in < 5 s with native call UI, answer, audio works. - Deploy: add livekit-server to compose (UDP 50000–60000, TCP 7881).