This document explains how to build an Android app that acts as an AndyClaw extension — a standalone APK that exposes functions the AndyClaw agent can discover and invoke at runtime.
The ExtensionExample module in this repository is a minimal, working reference implementation using the bound service bridge.
┌───────────────────────┐ ┌──────────────────────────┐
│ AndyClaw │ │ Your Extension APK │
│ │ │ │
│ 1. ApkExtensionScan │────────▶│ <meta-data> in manifest │
│ ner reads all │ │ + @raw/extension_mani- │
│ installed pkgs │ │ fest.json │
│ │ │ │
│ 2. ExtensionEngine │ │ │
│ registers the │ │ │
│ extension │ │ │
│ │ │ │
│ 3. ApkExtensionExe- │◀───────▶│ IPC bridge (service, │
│ cutor invokes │ IPC │ provider, receiver, or │
│ functions via │ │ activity intent) │
│ detected bridge │ │ │
└───────────────────────┘ └──────────────────────────┘
Discovery happens by scanning every installed package's <application> metadata for the key org.ethereumphone.andyclaw.EXTENSION. When found, AndyClaw reads the extension's identity, its function manifest, and detects what IPC bridge it exposes.
Execution routes through the highest-fidelity bridge available. The priority order is: bound service > content provider > broadcast receiver > explicit intent.
An extension is a regular Android app. Create a new com.android.application module (not a library). It can have its own UI, or it can be headless — the only requirement is the manifest metadata and at least one IPC bridge component.
Add four <meta-data> entries inside the <application> tag of your AndroidManifest.xml:
<application ...>
<!-- Required: marks this APK as an AndyClaw extension -->
<meta-data
android:name="org.ethereumphone.andyclaw.EXTENSION"
android:value="true" />
<!-- Required: unique identifier for your extension -->
<meta-data
android:name="org.ethereumphone.andyclaw.EXTENSION_ID"
android:value="my_extension" />
<!-- Required: human-readable name shown in the UI -->
<meta-data
android:name="org.ethereumphone.andyclaw.EXTENSION_NAME"
android:value="My Extension" />
<!-- Required: reference to a raw JSON resource listing your functions -->
<meta-data
android:name="org.ethereumphone.andyclaw.EXTENSION_MANIFEST"
android:resource="@raw/extension_manifest" />
...
</application>| Key | Type | Description |
|---|---|---|
EXTENSION |
"true" |
Boolean flag that marks the APK for discovery. |
EXTENSION_ID |
string | Globally unique ID. Convention: my_extension or com.example.myext. |
EXTENSION_NAME |
string | Display name shown in AndyClaw's Extensions settings. |
EXTENSION_MANIFEST |
@raw/... |
Resource reference to the function manifest JSON. |
Create res/raw/extension_manifest.json. This is a JSON array where each element describes one function your extension exposes:
[
{
"name": "greet",
"description": "Returns a friendly greeting for the given name.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name to greet"
}
},
"required": ["name"]
}
}
]Each function object supports these fields:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Function name the agent will use to invoke it. Must be unique across all extensions. |
description |
string | yes | What the function does — the agent reads this to decide when to call it. |
inputSchema |
object | yes | JSON Schema describing the parameters. |
requiresApproval |
boolean | no | If true, the user must explicitly approve each invocation. Default: false. |
requiredPermissions |
string[] | no | Android permissions the host app must hold. Default: []. |
You need at least one IPC bridge so AndyClaw can actually call your functions. Choose from the four options below. If your extension exposes multiple bridge types, AndyClaw will pick the highest-fidelity one automatically.
| Priority | Bridge Type | Best For |
|---|---|---|
| 1 (best) | Bound Service | Bidirectional, typed communication. Recommended for most extensions. |
| 2 | Content Provider | Synchronous request/response. Good if you already have a provider. |
| 3 | Broadcast Receiver | Asynchronous single-shot. Simple but limited. |
| 4 | Explicit Intent | Fire-and-forget. No structured return value. |
Declare a service with the extension intent-filter:
<service
android:name=".MyExtensionService"
android:exported="true">
<intent-filter>
<action android:name="org.ethereumphone.andyclaw.EXTENSION_SERVICE" />
</intent-filter>
</service>Implement a Service with a custom Binder that speaks AndyClaw's wire protocol:
class MyExtensionService : Service() {
companion object {
private const val DESCRIPTOR = "org.ethereumphone.andyclaw.IExtension"
private const val TRANSACTION_GET_MANIFEST = 1
private const val TRANSACTION_EXECUTE = 2
}
private val binder = object : Binder() {
override fun onTransact(
code: Int, data: Parcel, reply: Parcel?, flags: Int
): Boolean {
return when (code) {
TRANSACTION_GET_MANIFEST -> {
data.enforceInterface(DESCRIPTOR)
reply?.writeNoException()
reply?.writeString(getManifestJson())
true
}
TRANSACTION_EXECUTE -> {
data.enforceInterface(DESCRIPTOR)
val function = data.readString() ?: ""
val paramsJson = data.readString() ?: "{}"
reply?.writeNoException()
reply?.writeString(execute(function, paramsJson))
true
}
else -> super.onTransact(code, data, reply, flags)
}
}
}
override fun onBind(intent: Intent?): IBinder = binder
private fun getManifestJson(): String {
// Return the same manifest as your raw resource, or build it dynamically
...
}
private fun execute(function: String, paramsJson: String): String {
// Parse paramsJson, run your logic, return a JSON result string
...
}
}Wire protocol reference:
| Transaction Code | Request | Response |
|---|---|---|
1 (GET_MANIFEST) |
Interface token only | writeNoException() + writeString(manifestJson) |
2 (EXECUTE) |
Interface token + readString() (function name) + readString() (params JSON) |
writeNoException() + writeString(resultJson) |
The interface descriptor must be "org.ethereumphone.andyclaw.IExtension". The result from EXECUTE is a free-form JSON string — AndyClaw passes it through to the agent as-is.
Declare a provider whose authority ends with .andyclaw.extension:
<provider
android:name=".MyExtensionProvider"
android:authorities="${applicationId}.andyclaw.extension"
android:exported="true" />Implement call() to handle function invocations:
class MyExtensionProvider : ContentProvider() {
override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
val function = extras?.getString("function") ?: ""
val params = extras?.getString("params") ?: "{}"
val result = Bundle()
try {
result.putString("result", execute(function, params))
} catch (e: Exception) {
result.putString("error", e.message)
}
return result
}
private fun execute(function: String, params: String): String {
// Your logic here — return a JSON result string
...
}
// Required overrides (can be no-ops)
override fun onCreate() = true
override fun query(...) = null
override fun insert(...) = null
override fun update(...) = 0
override fun delete(...) = 0
override fun getType(...) = null
}AndyClaw calls ContentResolver.call() with the method "execute" and a bundle containing "function" and "params". Return a bundle with either "result" (success) or "error" (failure).
Declare a receiver with the extension intent-filter:
<receiver
android:name=".MyExtensionReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.ethereumphone.andyclaw.EXTENSION_BROADCAST" />
</intent-filter>
</receiver>Handle the broadcast and send a response back:
class MyExtensionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val function = intent.getStringExtra("function") ?: return
val params = intent.getStringExtra("params") ?: "{}"
val responseAction = intent.getStringExtra("response_action") ?: return
val result = execute(function, params)
val response = Intent(responseAction).apply {
putExtra("result", result)
}
context.sendBroadcast(response)
}
private fun execute(function: String, params: String): String {
// Your logic here — return a JSON result string
...
}
}AndyClaw sends a broadcast with extras "function", "params", and "response_action". Your receiver processes the request and broadcasts back a response intent on the response_action with either "result" or "error".
Declare an activity with the extension intent-filter:
<activity
android:name=".MyExtensionActivity"
android:exported="true">
<intent-filter>
<action android:name="org.ethereumphone.andyclaw.EXTENSION_ACTION" />
</intent-filter>
</activity>The activity receives "function" and "params" as intent extras. This bridge has no structured return value — AndyClaw reports success as soon as the activity launches. Use this for actions where a result isn't needed (e.g., opening a specific screen).
- Build and install your extension APK on the device.
- Open AndyClaw, go to Settings > Extensions, and tap Rescan.
- Your extension should appear in the list with its name and function count.
- The agent can now invoke your functions by name during conversations.
AndyClaw enforces three security checks on extensions by default:
| Check | What It Does |
|---|---|
| Signature Validation | Verifies the extension APK has valid signing certificates. If the extension descriptor includes a signingCertHash, the certificate's SHA-256 digest must match exactly. |
| UID Isolation | Ensures the extension runs under a different Linux UID than AndyClaw (no android:sharedUserId overlap). |
| Permission Checks | Verifies that Android runtime permissions declared in requiredPermissions are granted to the host app. |
Extensions can be marked as trusted by the user in the host app, which bypasses all checks. There is also a developer mode that disables all security checks globally — intended only for development.
- Function names must be globally unique across all installed extensions. Use a prefix if you're worried about collisions (e.g.,
myext_greetinstead ofgreet). - Write clear descriptions — the agent uses them to decide when to call your function. Be specific about what the function does, what it returns, and when it should be used.
- Keep execution fast — the default timeout is 30 seconds. Long-running work should be offloaded to a background thread.
- Return structured JSON — while AndyClaw doesn't enforce a result schema, returning well-structured JSON helps the agent interpret and relay results to the user.
- Your extension is a normal app — it can have its own UI, background services, databases, and anything else a regular Android app can have. The extension bridge is just one component.