Thin Android runtime / executor for a personal agent system.
This app is intentionally not a fat automation brain. It is a small Android-side execution surface that:
- accepts JSON actions,
- performs bounded device capabilities,
- returns machine-readable JSON results,
- leaves orchestration and decision logic to desktop/server agents.
Implemented in v0.2.0-alpha2:
health_pingdevice_infoopen_urllaunch_app(package)list_installed_appsusage_stats(requires user-granted usage access)uninstall_app(Intent-based, user confirmation required)open_intent/test_intentcheck_self_update(GitHub Releases latest endpoint)download_self_update(downloads APK and opens install prompt)- JSON-first success/error output
- In-app recent command log panel for manual testing
- Remote transport config UI
- Polling-based remote command fetch / result upload alpha
- Foreground remote polling service
- Current app version display
- Check update / Update now controls
- Manifest-driven soft-force update policy
- Thin runtime on device
- Capability-first instead of hardcoded feature flows
- JSON command contract first
- Explicit permission / consent boundaries
- Safe path for future expansion into Accessibility / FCM
app/Android appcatalog/canonical capability registry source data (v0.1 scaffold)docs/product / architecture / trust-boundary / roadmap docs.github/workflows/android-release.ymlCI/CD for APK build + release asset publishingscripts/release.ps1helper for tag-based release flowscripts/generate-update-manifest.mjshelper for release manifest generationscripts/validate-catalog.mjsscaffold validator for registry recordsscripts/build-catalog-json.mjsscaffold JSON export placeholder
Start with docs/README.md for the doc-first reading path.
- JDK 17+ (Java 21 also works locally for wrapper generation, CI uses JDK 17)
- Android SDK installed locally
local.propertiescreated fromlocal.properties.example
copy local.properties.example local.properties
# then edit sdk.dir.\gradlew.bat assembleDebugadb install -r .\app\build\outputs\apk\debug\app-debug.apkUse the signed GitHub Release APK for normal user installation and in-app self-update. ADB is developer tooling, not the primary install path.
Used for list_installed_apps and app launching by package name.
This is sensitive on Play-distributed apps, but acceptable for a personal sideloaded companion runtime.
Needed for usage_stats.
Android does not grant this like a normal runtime permission. The user must explicitly allow usage access in system settings.
Needed for self-update install prompt when sideloading newer APKs.
Preferred envelope in v0.1.1:
{
"action": "open_url",
"request_id": "demo-001",
"params": {
"url": "https://docs.openclaw.ai"
}
}Legacy flat v0.1 shape is still accepted for backward compatibility.
{
"action": "health_ping",
"params": {}
}{
"action": "device_info",
"params": {}
}{
"action": "open_url",
"params": {
"url": "https://docs.openclaw.ai"
}
}{
"action": "launch_app",
"params": {
"package": "com.android.settings"
}
}{
"action": "list_installed_apps",
"params": {
"include_system": false
}
}{
"action": "usage_stats",
"params": {
"hours": 12
}
}{
"action": "uninstall_app",
"params": {
"package": "com.example.target"
}
}{
"action": "open_intent",
"params": {
"action": "android.settings.APPLICATION_DETAILS_SETTINGS",
"uri": "package:com.android.chrome",
"delivery_policy": "auto"
}
}open_intent delivery policies:
direct: attempt an immediatestartActivity()launchnotify: post a notification and require a user tap to execute the intentauto: choosedirectwhen the app is foreground, otherwisenotify
This matters because modern Android background activity launch policy can let a service call succeed technically while still failing to produce a visible UI transition for the user.
{
"action": "check_self_update",
"params": {
"release_api_url": "https://api.github.com/repos/var-gg/android-companion/releases/latest"
}
}{
"action": "download_self_update",
"params": {
"apk_url": "https://github.com/var-gg/android-companion/releases/download/v0.1.0/app-debug.apk"
}
}{
"ok": true,
"action": "launch_app",
"timestamp": "2026-03-17T09:00:00Z",
"package": "com.android.settings"
}{
"ok": false,
"error": {
"code": "missing_package",
"message": "package is required"
},
"timestamp": "2026-03-17T09:00:00Z"
}The app now supports a simple in-app update UX for sideload releases:
- shows current installed version and versionCode
- checks
update-manifest.jsonfirst for update policy - supports
force_updateandmin_supported_version_codesoft-gating - can download the latest APK and open Android's install prompt
Current manifest source:
https://raw.githubusercontent.com/var-gg/android-companion/main/update-manifest.json
Soft-force update means the app can block normal command execution and remote start/register actions until the minimum supported version is installed, while still allowing update actions.
The app includes a polling-based remote transport alpha:
- configure base URL / device ID / bearer token / poll interval in the app
- register the device to a bridge endpoint
- start the foreground polling service
- app fetches remote commands, executes them, and uploads results
Reference:
docs/13-remote-transport-alpha.mddocs/14-pairing-over-tailscale.mdscripts/mock-remote-bridge.mjsscripts/generate-pairing-link.mjsscripts/generate-pairing-page.mjs
Transport settings are stored in app-local persistent storage. This is the recommended default for a public sideloaded Android companion app:
base_url: persisteddevice_id: persistedpoll_interval_seconds: persistedtoken: currently persisted in app storage
Notes:
device_idis auto-generated on first load if the field is empty.- Normal app updates should preserve stored settings.
- App uninstall, package name changes, or switching between different package ids can reset stored data.
- For long-term hardening, sensitive values like tokens should move to encrypted storage / Keystore-backed storage.
Assume many users of this app will also use an AI agent system such as OpenClaw. An AI agent helping a user set up this app should gather or derive these values and then give the user copy/paste-ready inputs.
Required inputs for the app:
Remote Base URLDevice IDBearer Token(recommended; effectively required for any non-trusted network)Poll interval seconds
When assisting with setup, the agent should explicitly provide:
- the exact
Remote Base URLto paste into the app - whether the user should keep the auto-generated
Device ID(default: yes) - the exact
Bearer Tokento paste only if auth is enabled; otherwise explicitly say to leave it blank - the recommended
Poll interval seconds - whether the bridge is LAN-only, reverse-proxied, or public
- whether additional Android permissions/settings should be enabled for stable polling
- Start or identify the bridge endpoint.
- Determine the phone-reachable URL.
- Choose a stable
device_id. - Generate or retrieve a bearer token.
- Tell the user exactly what to paste into each app field.
- Ask the user to tap
Save,Register device, andStart remote. - Validate that the bridge sees the device and that commands round-trip successfully.
Start a local mock bridge on the desktop. For the easiest local/LAN test, do not set a token at all:
node .\scripts\mock-remote-bridge.mjsThen provide these values to the user:
Remote Base URL:http://<desktop-ip>:8787Device ID: keep the app-generated valueBearer Token: leave blank for local/LAN dev modePoll interval seconds:10
Then enqueue a command from desktop:
$headers = @{ Authorization = "Bearer dev-secret" }
$body = @{
device_id = "<your-device-id>"
action = "list_installed_apps"
params = @{ include_system = $false }
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Method Post -Uri "http://<desktop-ip>:8787/api/v1/commands/enqueue" -Headers $headers -ContentType "application/json" -Body $bodyInspect uploaded results:
Invoke-RestMethod -Uri "http://<desktop-ip>:8787/api/v1/results" -Headers $headersWorkflow: .github/workflows/android-release.yml
- push tag:
v0.1.0 - manual workflow dispatch
- manual workflow runs may build debug artifacts for local/dev testing
- tag releases build and publish signed release APKs only
- if signing secrets are missing on a tag release, CI fails instead of publishing a broken unsigned/debug public asset
- on tag push, the signed APK is published into GitHub Releases
See docs/15-signing-and-release-channel.md.
Required for official/public release APKs:
ANDROID_KEYSTORE_BASE64ANDROID_KEYSTORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORD
Without these, do not cut a public tag intended for browser install or in-app self-update.
- repo tag:
v0.1.0 - official release asset (signed):
android-companion-v0.1.0.apk - debug artifacts: workflow artifacts / local testing only
- app opens on device
-
health_pingreturns JSON -
device_inforeturns model/sdk/package/version -
open_urlopens browser -
launch_applaunches settings or another installed app -
list_installed_appsreturns packages -
usage_statsreturns permission error before grant - grant usage access, then
usage_statsreturns data -
uninstall_appopens uninstall confirmation UI -
open_intentcan open settings or target activity -
check_self_updatedetects latest GitHub Release -
download_self_updatedownloads APK and opens install prompt
- No background transport yet (no FCM/WebSocket command ingress)
- No authenticated remote command channel yet
- Self-update uses GitHub Releases and sideload flow, not Play updates
- Public releases depend on stable signing secrets/keystore continuity in GitHub
- Capability handlers are partially extracted, but the app is still not fully modularized into separate Android modules yet
- extract capability handlers into modules/packages
- add authenticated HTTP/WebSocket or FCM transport
- richer intent result reporting
- package search / filtering / label lookup
- Accessibility automation bridge with explicit consent model
- foreground service + task queue for remote command execution