Sign inSign up

superbizon007/android-agent

By superbizon007

Updated 5 months ago

Android Agent

Image
Machine learning & AI
0

1.6K

superbizon007/android-agent repository overview

Android Agent

A Docker image that runs a VLM-powered Android device automation agent, exposed via A2A and OpenAI-compatible APIs. Live device view streams to the browser through an on-pod scrcpy bridge: the FastAPI process spawns scrcpy-server.jar on the device over ADB, pipes the H.264 elementary stream to a WebSocket client, and renders it with the browser's native WebCodecs VideoDecoder — no on-device APK, no MediaProjection consent, no WebRTC.

Implements an agent using the ReAct (Reasoning + Acting) pattern for intelligent Android device automation via ADB.

Overview

The container bundles:

  • ADB (Android Debug Bridge) — connects to and controls the Android device
  • scrcpy bridgeapp/server/scrcpy_bridge.py pushes scrcpy-server-v2.4.jar (baked at /opt/scrcpy-server.jar) to /data/local/tmp, runs it via app_process, opens a TCP tunnel through adb reverse, and forwards the H.264 packets to the browser. Browser-side input events come back over the same WebSocket and dispatch through the shared ADBClient (same code path as the VLM tools) — see PLAN_SCRCPY_STREAMING.md
  • WebSocket /stream + static /stream.html — JWT-authed WebSocket endpoint for the bridge plus a self-contained viewer page using the browser's WebCodecs API
  • Python agent server — FastAPI service that accepts tasks and autonomously controls the Android device using a Vision Language Model (VLM) via LangGraph

The agent observes the device through screenshots, reasons about the next action, and executes it — repeating until the task is complete or the step budget is exhausted. The streaming path is independent of the VLM control loop; it adds a real-time human-watchable view + a click-to-tap path on top.


Features

FeatureDetails
Android automationFull ADB control — tap, double-tap, swipe, long-press, scroll, key events, text input
VLM-driven agentAny OpenAI-compatible VLM (GPT-4o, Claude, Gemini, local models)
UI Locator modelOptional secondary model (UI-TARS) resolves natural-language element descriptions to pixel coordinates
Loop detectionWarns the VLM after 2 consecutive identical actions
SSE progress updatesPer-step status messages streamed to A2A clients
A2A APIGoogle Agent-to-Agent JSON-RPC 2.0 protocol
OpenAI-compatible API/v1/chat/completions endpoint — drop-in for OpenAI clients
HTTPSSelf-signed TLS at :443 for local dev (LOCAL_TLS=true); plain HTTP at :8080 in production with TLS terminated at the per-pod nginx Ingress
Trajectory recordingSaves per-step screenshots, VLM requests/responses, and locator logs
Live device viewscrcpy server piped over WebSocket → browser WebCodecs decoder. Hardware-encoded H.264 from any modern Android (API 21+) — no APK install, no consent dialog
Click-to-tapBrowser's pointer / keyboard events travel as JSON over the same WebSocket and dispatch through ADBClient.tap / swipe / key_press — same pixel coords as apps/android_agent/app/agent/tools.py

Architecture

                                              ┌──────────────────────────┐
Android device (USB or network ADB)           │ User's browser (Chrome)  │
┌──────────────────────────────┐               │   stream.html?token=…    │
│ scrcpy-server-v2.4.jar       │   H.264       │  • WebCodecs VideoDecoder│
│  (pushed via adb push +      │   Annex-B     │    → <canvas>            │
│   spawned via app_process)   │◄──────────────┤  • pointer/key events    │
└──────────────┬───────────────┘  TCP via      │    → JSON over same WS   │
               │                  adb reverse  └──────────┬───────────────┘
               │                                          │
               │                                          │  WS /stream
               │                                          │  ?token=<viewer_jwt>
               ▼                                          ▼
        ┌──────────────────────────────────────────────────────┐
        │ Docker container                                     │
        │                                                      │
        │  ADB ─► ADBClient ─► LangGraph (graph.py)            │
        │                       screenshot → VLM →             │
        │                       execute_action (tap/swipe/…)   │
        │                                                      │
        │  FastAPI on :8080 plain (LOCAL_TLS=false, prod)      │
        │       or :443 self-signed (LOCAL_TLS=true, dev)      │
        │    ├── A2A      (POST /)                             │
        │    ├── OpenAI   (/v1/…)                              │
        │    ├── /stream  (JWT-authed WS — scrcpy bridge)      │
        │    └── /stream.html (static WebCodecs viewer)        │
        └──────────────────────────────────────────────────────┘

The bridge spawns a fresh scrcpy server per WebSocket connection and binds an ephemeral local TCP port, so multiple viewer sessions on one pod don't collide.


Quick Start

Quickest path
cd apps/android_agent
./scripts/run-local.sh ADB_SERIAL [VLM_API_KEY]

Builds the image, mints the device + viewer JWT pair, runs the container with LOCAL_TLS=false, and prints the viewer URL: http://localhost:8765/stream.html?token=<viewer_jwt>. Open it in Chrome — no consent dialog, no APK install, video starts immediately.

ADB_SERIAL accepts any of:

  • 7a4e6acb — USB device (the Mac's adb daemon is proxied into the container)
  • 192.168.1.10:5555 — network ADB
  • emulator-5554 — local emulator

For USB / emulator serials, run this once on the host before starting the container so the container's adb can reach your daemon:

adb kill-server && adb -a -P 5037 nodaemon server &

Optional env (read by the script before invoking docker):

Env varDefaultPurpose
VLM_URLhttps://api.openai.com/v1Override for non-OpenAI VLMs
VLM_MODELgpt-4oModel name
API_KEYSets the agent's bearer token
HOST_PORT8765Host port mapped to container :8080
TRAJ_HOST_DIRHost dir bind-mounted at /data/traj; the script also sets TRAJECTORY_DIR=/data/traj so per-step screenshots + VLM IO land on the host
REBUILD0Force docker build even if the image is already cached

Example:

VLM_URL=https://my-vlm.example.com/v1 VLM_MODEL=gpt-4o-mini \
TRAJ_HOST_DIR=$PWD/agent-logs \
./scripts/run-local.sh 192.168.1.10:5555 sk-...
Prerequisites

The Android device must have ADB debugging enabled:

  • USB: connect directly and accept the RSA key prompt on the device
  • Network: enable Wireless debugging in Developer Options (Android 11+), then connect via adb connect HOST:PORT
  • Emulator / cloud Android: any image that boots scrcpy-compatible video encoders (most do; emulated CPU images may lack hardware H.264)

The on-device side is just scrcpy-server-v2.4.jar running under app_process — no APK install, no system-app signing, no Device Owner enrollment.

Build
cd apps/android_agent
docker build \
  --build-context a2a_pkg=../../packages/a2a_agent \
  -f Dockerfile.ubuntu \
  -t android_agent:ubuntu .

The image bakes vendor/scrcpy-server-v2.4.jar to /opt/scrcpy-server.jar. Single-stage build — no Android SDK needed at build time.

Run — network ADB
docker run -d \
  --name android_agent \
  -p 8765:8080 \
  -e ADB_SERIAL=192.168.1.10:5555 \
  -e VLM_URL=https://api.openai.com/v1 \
  -e VLM_API_KEY=sk-... \
  -e VLM_MODEL=gpt-4o \
  -e LOCAL_TLS=false \
  -e STREAM_HMAC_SECRET=$(openssl rand -hex 32) \
  -e RUN_ID=run-local-1 \
  -e POD_PUBLIC_HOST=localhost:8765 \
  android_agent:ubuntu --wait
Run — host ADB server (USB device or emulator)

When the device is connected on the host (adb devices shows it), forward the host ADB server into the container instead of using network ADB.

Step 1 — restart host ADB server to listen on all interfaces:

adb kill-server
adb -a -P 5037 nodaemon server &
adb devices   # verify your device is still listed

Step 2 — run the container (macOS / Windows Docker Desktop):

docker run -d \
  --name android_agent \
  -p 8765:8080 \
  -e ADB_SERIAL=emulator-5554 \
  -e ANDROID_ADB_SERVER_ADDRESS=host.docker.internal \
  -e ANDROID_ADB_SERVER_PORT=5037 \
  -e VLM_API_KEY=sk-... \
  -e LOCAL_TLS=false \
  -e STREAM_HMAC_SECRET=$(openssl rand -hex 32) \
  -e RUN_ID=run-local-1 \
  -e POD_PUBLIC_HOST=localhost:8765 \
  android_agent:ubuntu --wait

Linux — use --network host instead:

docker run -d \
  --network host \
  -e ADB_SERIAL=emulator-5554 \
  -e VLM_API_KEY=sk-... \
  -e LOCAL_TLS=false \
  -e STREAM_HMAC_SECRET=$(openssl rand -hex 32) \
  -e RUN_ID=run-local-1 \
  -e POD_PUBLIC_HOST=localhost:8080 \
  android_agent:ubuntu --wait

ANDROID_ADB_SERVER_ADDRESS and ANDROID_ADB_SERVER_PORT are standard ADB client environment variables. The adb binary inside the container reads them automatically and connects to the host's ADB server, which proxies all communication to the device.

Watching the device

Mint a viewer-role JWT (same STREAM_HMAC_SECRET, same RUN_ID, role:"viewer") and open http://localhost:8765/stream.html?token=<viewer_jwt> in Chrome.

A minimal token-minting helper (Python ≥ 3.8, no deps):

python3 - <<'PY'
import jwt, time, os
print(jwt.encode(
    {"run_id": "run-local-1", "role": "viewer", "ice": [],
     "iat": int(time.time()), "exp": int(time.time()) + 14400},
    os.environ["STREAM_HMAC_SECRET"], algorithm="HS256",
))
PY

The bridge validates token signature, role, run_id, and expiry on connect. Close codes: 4401 (auth), 4503 (streaming not configured on this pod). See tests/test_stream.py and tests/test_stream_auth.py for boundary coverage.

Dev image (VNC only, no API server)

For testing ADB connectivity manually with a noVNC viewer (no agent, no scrcpy bridge):

docker build \
  --build-context a2a_pkg=../../packages/a2a_agent \
  -f Dockerfile.dev \
  -t android_agent:dev .

docker run -d \
  --name android_agent_dev \
  -p 5901:5901 \
  -p 6901:6901 \
  -e ADB_SERIAL=192.168.1.10:5555 \
  android_agent:dev --wait

Open http://localhost:6901/?password=vncpassword.

Trust the certificate (one-time, LOCAL_TLS=true only)

When LOCAL_TLS=false, the pod serves plain HTTP and TLS terminates at the per-pod Ingress with a real (Let's Encrypt or wildcard) cert — no manual trust step needed. The block below applies only to local dev.

macOS:

curl -k https://localhost/ca.crt -o android-agent-ca.crt
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain android-agent-ca.crt

Linux (Debian/Ubuntu):

curl -k https://localhost/ca.crt -o /usr/local/share/ca-certificates/android-agent-ca.crt
sudo update-ca-certificates

Windows:

curl -k https://localhost/ca.crt -o android-agent-ca.crt
certutil -addstore Root android-agent-ca.crt

API

A2A — send a task
curl -sk -X POST https://localhost/ \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "message/send",
    "params": {
      "message": {
        "role": "user",
        "messageId": "m1",
        "parts": [{"kind": "text", "text": "Open Settings and show me the Android version"}]
      }
    }
  }'
OpenAI-compatible
curl -sk -X POST https://localhost/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "android-agent",
    "messages": [{"role": "user", "content": "Open the YouTube app and search for cats"}]
  }'
Agent card
curl -sk https://localhost/.well-known/agent-card.json
Streaming (A2A)
curl -sk -N -X POST https://localhost/ \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "message/stream",
    "params": {
      "message": {
        "role": "user",
        "messageId": "m1",
        "parts": [{"kind": "text", "text": "Take a screenshot and describe what you see"}]
      }
    }
  }'

Environment Variables

Core
VariableDefaultDescription
VLM_URLhttps://api.openai.com/v1Base URL of the VLM API
VLM_API_KEYAPI key for the VLM
VLM_MODELgpt-4oModel name
ADB_SERIALDevice serial or HOST:PORT (e.g. 192.168.1.10:5555). If unset, uses the first connected device
ADB_PATHadbPath to the adb binary
API_KEYBearer token to protect the agent API (optional)
PORT443HTTPS port for the agent server
AGENT_URLPublic URL published in the AgentCard (e.g. https://my-agent.example.com/). Defaults to https://localhost:{PORT}/
VIEWPORT_WIDTH1080Fallback screen width — overwritten at startup by adb shell wm size
VIEWPORT_HEIGHT1920Fallback screen height — overwritten at startup by adb shell wm size
SEND_IMAGEtrueSet to false for text-only LLMs (disables screenshot in VLM prompt)
TRAJECTORY_DIRDirectory to save trajectory data; each run creates a YYYYMMDD_HHMMSS/ subdirectory
UI Locator model (optional)

The locator is a secondary vision model (e.g. UI-TARS) specialised in resolving natural-language element descriptions to exact pixel coordinates. When configured, the agent describes elements by name (query) instead of estimating coordinates, and the locator finds the exact tap target.

VariableDefaultDescription
LOCATOR_URLOpenAI-compatible API base URL for the locator model
LOCATOR_API_KEYBearer token for locator API auth (optional)
LOCATOR_MODELui-tarsModel name passed to the locator API
Shared workspace (optional)

Used when the agent runs as part of a multi-agent pipeline. Agents read/write shared artifacts and post their status to a blackboard visible to the orchestrator.

VariableDefaultDescription
WORKSPACE_IDWorkspace identifier (e.g. project-abc). Required to enable workspace features
WORKSPACE_MOUNT_PATH/workspacePath where the S3 bucket is FUSE-mounted (used when S3_BUCKET is not set)
S3_BUCKETS3 bucket name for direct S3 access (skips FUSE mount)
S3_ENDPOINT_URLS3-compatible endpoint URL (omit for AWS S3)
S3_ACCESS_KEY_IDS3 access key
S3_SECRET_ACCESS_KEYS3 secret key
S3_REGIONus-east-1S3 region
S3_PREFIXOptional prefix before WORKSPACE_ID in the S3 key path
AGENT_IDandroid_agentUnique name written to the blackboard for this agent instance
Agent identity (optional)
VariableDefaultDescription
AGENT_NAMEcomputer.android.guiName published in the AgentCard
AGENT_VERSION0.1.0Version string published in the AgentCard
AGENT_DESCRIPTION(built-in)Description published in the AgentCard
Streaming

These wire the JWT-authed scrcpy bridge on /stream and the static viewer page on /stream.html. See PLAN_SCRCPY_STREAMING.md for the full design.

VariableDefaultDescription
LOCAL_TLStrueWhen true, pod generates self-signed cert and listens HTTPS on :443 (local docker-run flow). When false (Core injects this for streaming pods), pod listens plain HTTP on :8080 and TLS terminates at the per-pod nginx Ingress
STREAM_HMAC_SECRETShared secret with Core for HMAC-validating stream JWTs. Empty disables /stream (close code 4503)
RUN_IDPer-run identifier; must match the run_id claim in stream JWTs
POD_PUBLIC_HOSTExternally reachable hostname (e.g. <run_id>.agents.forfetch.ai). Used in token minting + AgentCard stream_url
STREAM_DEVICE_TOKENReserved for symmetry with the legacy WebRTC plan; the scrcpy bridge has no on-device side that consumes it
SCRCPY_SERVER_JAR/opt/scrcpy-server.jarPath to scrcpy server JAR. The bridge adb push'es this to /data/local/tmp per session
STREAM_BITRATE_KBPS8000Currently informational — scrcpy_bridge.py hard-codes video_bit_rate=8000000 when spawning the server. Surface here so a future config refactor flows it through
STREAM_FPS30Informational — see above
STREAM_RESOLUTION1080x1920Informational — bridge currently passes max_size=0 to scrcpy (= native resolution)

Agent Tools

ToolDescription
screenshotJPEG screenshot of the Android device (base64) — taken automatically at every step
tapSingle tap at (x, y)
double_tapDouble-tap at (x, y)
swipeSwipe from (x1,y1) to (x2,y2) over duration_ms ms — for transitions and drag-and-drop
long_pressLong-press at (x, y) for duration_ms ms — opens context menus, selects text
scrollScroll at (x, y) in direction up/down/left/right by distance pixels
key_pressPress an Android key: back, home, app_switch, enter, delete, volume_up, etc.
type_textType text into the currently focused input field (tap to focus first)
get_current_appReturn the currently focused app and window from dumpsys window
run_adb_shellRun an arbitrary shell command on the device via adb shell

Trajectory Recording

When TRAJECTORY_DIR is set, each agent run creates a timestamped subdirectory:

$TRAJECTORY_DIR/
  20260317_141523/              # run timestamp (YYYYMMDD_HHMMSS)
    summary.md                  # task, result, and final plan summary
    turn_0000/
      screenshot.png            # device state at start of step
      llm_0_request.json        # messages sent to the VLM
      llm_0_response.json       # raw VLM reply + parsed action/args/reasoning
      locator.json              # locator model request + response (when LOCATOR_URL is set)
    turn_0001/
      ...

Example:

docker run -d \
  -e VLM_URL=... -e VLM_API_KEY=... \
  -e ADB_SERIAL=192.168.1.10:5555 \
  -e TRAJECTORY_DIR=/data/trajectory \
  -v /tmp/agent-logs:/data/trajectory \
  android_agent:ubuntu --wait

License

MIT

MCP tools

The agent image exposes an MCP (Model Context Protocol) server on the same port as A2A at /mcp. Any MCP client — Claude Desktop, Cursor, Windsurf, ChatGPT Connectors, OpenAI Agents SDK — can list and invoke these tools using the pod's API_KEY as Bearer.

In production, Core proxies https://android-agent.agents.forfetch.ai/mcp → the worker pod's /mcp.

Tool (skill_id)Description
tapTap on coordinates or an element
swipeSwipe in a direction
typeType text using the keyboard
screenshotTake a screenshot of the device screen
backPress the Android back button
homePress the Android home button
launch_appLaunch an application by package name
scrollScroll the screen

Input schemas are authored in apps/agents_mcp/app/seed.py. Skills without an explicit schema advertise a single {task: str} freeform parameter.

Tag summary

Content type

Image

Digest

sha256:dda977d47

Size

558.5 MB

Last updated

5 months ago

docker pull superbizon007/android-agent