Skip to main content

Cargo Runtime Environment

Deployments in the Cargo runtime run as native binaries inside a Linux distro image on the processor, isolated via PRoot. Standard system environment variables are available directly. Host services - deployment metadata, cryptographic signing, and browser control - are accessed through an RPC API using JSON-RPC 2.0 over an abstract Unix domain socket.

Distro Image

Each Cargo deployment must specify a Linux distro image in its manifest, along with the SHA256 hash of the image for verification. The processor downloads and extracts the image before running the deployment.

manifest.json
{
"version": "1",
"entrypoint": "example.sh",
"image": {
"url": "https://example.com/distro.tar.xz",
"sha256": "abc123..."
}
}

Supported distro images can be found in the Termux proot-distro repository.

Environment Variables

Env vars declared in your deployment config are injected as standard system environment variables. See Environment Variables for how to declare them.

let api_key = std::env::var("API_KEY").unwrap();

PRoot Quirks

The PRoot container starts with a minimal environment. The processor injects the following defaults before the entrypoint runs:

Variable / fileDefault value
PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME/root
/etc/resolv.confGoogle (8.8.8.8, 8.8.4.4), Cloudflare (1.1.1.1, 1.0.0.1), Quad9 (9.9.9.9) - skipped if the file already contains a non-loopback nameserver

These can be overridden in your entrypoint script if your deployment requires different values.

DNS resolution

Android has no system-level /etc/resolv.conf. Without it, DNS resolution inside the chroot fails silently - apt-get update downloads nothing, package lists stay empty, and subsequent apt-get install calls report packages as not found. The processor writes a set of public resolvers to /etc/resolv.conf at prepare time to avoid this, unless the file already contains a non-loopback nameserver entry.

To use different resolvers, write them yourself before any network calls:

echo "nameserver <your-resolver>" > /etc/resolv.conf

Network interfaces (getifaddrs)

Programs running inside the chroot that call getifaddrs() - SSH daemons, some package managers - receive Android's real network interfaces (wlan0, rmnet_data0, etc.) instead of a standard Linux interface list. This happens because PRoot bind-mounts /proc from the Android host and glibc queries the kernel via netlink, which responds with Android interfaces. Programs may fail to bind or behave unexpectedly as a result.

To fix this, override getifaddrs and freeifaddrs in a shared library and preload it via LD_PRELOAD:

getifaddrs_override.c
#include <ifaddrs.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <net/if.h>

int getifaddrs(struct ifaddrs **ifap) {
struct ifaddrs *ifa = calloc(1, sizeof(struct ifaddrs));
if (!ifa) return -1;
ifa->ifa_next = NULL;
ifa->ifa_name = strdup("lo");
ifa->ifa_flags = IFF_UP | IFF_RUNNING | IFF_LOOPBACK;

struct sockaddr_in *addr = calloc(1, sizeof(struct sockaddr_in));
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = htonl(0x7f000001);
ifa->ifa_addr = (struct sockaddr *)addr;

struct sockaddr_in *netmask = calloc(1, sizeof(struct sockaddr_in));
netmask->sin_family = AF_INET;
netmask->sin_addr.s_addr = htonl(0xff000000);
ifa->ifa_netmask = (struct sockaddr *)netmask;

*ifap = ifa;
return 0;
}

void freeifaddrs(struct ifaddrs *ifa) {
while (ifa) {
struct ifaddrs *next = ifa->ifa_next;
free(ifa->ifa_name);
free(ifa->ifa_addr);
free(ifa->ifa_netmask);
free(ifa);
ifa = next;
}
}

Compile and preload it before starting the affected program:

gcc -shared -fPIC -o /usr/local/lib/getifaddrs_override.so getifaddrs_override.c
export LD_PRELOAD=/usr/local/lib/getifaddrs_override.so

RPC API

The processor injects a BRIDGE_SOCKET environment variable at runtime containing the name of an abstract Unix socket. To call a host API, open a new socket connection, send a single JSON-RPC 2.0 request line, and read back the response line. Each connection carries exactly one request/response exchange.

socket address:  \0<BRIDGE_SOCKET>   (abstract namespace - null-byte prefix)
protocol: JSON-RPC 2.0, newline-delimited, one call per connection

Processor

Android: 1.25.0+

processor_version

Returns the version of the processor runtime.

Request
{
"jsonrpc": "2.0",
"method": "processor_version",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"version": string
},
"id": string
}

Deployment

Android: 1.25.0+

deployment_id

Returns the identifier of the active deployment.

Request
{
"jsonrpc": "2.0",
"method": "deployment_id",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"id": string,
"origin": {
"kind": string,
"source": string // hex
}
},
"id": string
}

Android: 1.25.0+

deployment_ipfsHash

Returns the IPFS CID of the deployment app.

Request
{
"jsonrpc": "2.0",
"method": "deployment_ipfsHash",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"ipfsHash": string
},
"id": string
}

Android: 1.25.0+

deployment_slot

Returns the execution slot index assigned to this processor, or null if no slot is assigned.

Request
{
"jsonrpc": "2.0",
"method": "deployment_slot",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"slot": number | null
},
"id": string
}

Android: 1.25.0+

deployment_publicKeys

Returns the signing public keys of this processor for the active deployment, keyed by curve. Only curves for which a key exists are included.

Request
{
"jsonrpc": "2.0",
"method": "deployment_publicKeys",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"publicKeys": {
"p256"?: string, // hex
"secp256k1"?: string, // hex
"ed25519"?: string // hex
}
},
"id": string
}

Android: 1.25.0+

deployment_encryptionKeys

Returns the ECDH encryption public keys of this processor for the active deployment. Supported curves: p256, secp256k1.

Request
{
"jsonrpc": "2.0",
"method": "deployment_encryptionKeys",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"encryptionKeys": {
"p256"?: string, // hex
"secp256k1"?: string // hex
}
},
"id": string
}

Android: 1.25.0+

deployment_assignedProcessors

Returns all processors assigned to this deployment, keyed by their SS58 address, along with their public keys per curve.

Request
{
"jsonrpc": "2.0",
"method": "deployment_assignedProcessors",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"processors": {
[ss58: string]: {
"p256"?: string, // hex, signing key
"secp256k1"?: string, // hex, signing key
"ed25519"?: string, // hex, signing key
"encP256"?: string, // hex, encryption key
"encSecp256k1"?: string // hex, encryption key
}
}
},
"id": string
}

Signer

Supported curves: "p256", "secp256k1", "ed25519". Encryption and decryption support "p256" and "secp256k1" only.

Android: 1.25.0+

signer_publicKey

Returns the public key for the given curve. Pass derivationPath for HD key derivation (supported on secp256k1).

Request
{
"jsonrpc": "2.0",
"method": "signer_publicKey",
"params": [{
"curve": "p256" | "secp256k1" | "ed25519",
"derivationPath"?: string
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"publicKey": string, // hex
"derivationPath"?: string // present only if requested
},
"id": string
}

Android: 1.25.0+

signer_sign

Signs a hex-encoded byte string with the given curve key. Pass derivationPath for HD signing.

Request
{
"jsonrpc": "2.0",
"method": "signer_sign",
"params": [{
"curve": "p256" | "secp256k1" | "ed25519",
"bytes": string, // hex
"derivationPath"?: string
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"bytes": string // hex
},
"id": string
}

Android: 1.25.0+

signer_encrypt

Encrypts bytes using ECDH key agreement with the receiver's public key. Supported curves: p256, secp256k1.

Request
{
"jsonrpc": "2.0",
"method": "signer_encrypt",
"params": [{
"curve": "p256" | "secp256k1",
"publicKey": string, // hex, receiver's public key
"salt": string, // hex
"bytes": string // hex, plaintext
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"bytes": string // hex, ciphertext
},
"id": string
}

Android: 1.25.0+

signer_decrypt

Decrypts bytes using ECDH key agreement with the sender's public key. Supported curves: p256, secp256k1.

Request
{
"jsonrpc": "2.0",
"method": "signer_decrypt",
"params": [{
"curve": "p256" | "secp256k1",
"publicKey": string, // hex, sender's public key
"salt": string, // hex
"bytes": string // hex, ciphertext
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"bytes": string // hex, plaintext
},
"id": string
}

Browser

These methods control an embedded WebView on the processor device and are only available on Android processors.

Android: 1.25.0+

browser_debugUrl

Returns the Chrome DevTools remote debugging URL for the WebView.

Request
{
"jsonrpc": "2.0",
"method": "browser_debugUrl",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"url": string
},
"id": string
}

Android: 1.27.0

browser_initSurface

Sets the size of the surface the browser paints its pages to, applied when the surface is created as the first tab is opened. Only takes effect if called before any tab is open - without it, the surface is created at the device's display size.

Fails if the surface size has already been initialized, if a surface already exists, or if the requested size is outside the supported bounds: each side between 240 and 2560 device pixels, total pixel count no more than 2560x1440, and deviceScaleFactor between 0.5 and 4.

Request
{
"jsonrpc": "2.0",
"method": "browser_initSurface",
"params": [{
"width": number, // device pixels
"height": number, // device pixels
"deviceScaleFactor"?: number // device pixels per CSS pixel, default: 1
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.27.0

browser_surfaceSize

Returns the size of the surface the browser paints its pages to, or null if the browser currently has no surface.

Request
{
"jsonrpc": "2.0",
"method": "browser_surfaceSize",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"size": {
"width": number, // device pixels
"height": number, // device pixels
"deviceScaleFactor": number // device pixels per CSS pixel
} | null
},
"id": string
}

Android: 1.25.0+

browser_newTab

Opens a new tab in the WebView and returns its ID.

Request
{
"jsonrpc": "2.0",
"method": "browser_newTab",
"params": [{ "url": string }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"id": number
},
"id": string
}

Android: 1.25.0+

browser_closeTab

Closes a tab by ID. Set wholeTree to true to also close all tabs spawned by this tab.

Request
{
"jsonrpc": "2.0",
"method": "browser_closeTab",
"params": [{
"id": number,
"wholeTree"?: boolean // default: false
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_openTabs

Returns a list of currently open tab IDs.

Request
{
"jsonrpc": "2.0",
"method": "browser_openTabs",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"tabs": Array<number | null>
},
"id": string
}

Android: 1.25.0+

browser_currentUrl

Returns the current URL of a tab, or null if it has not loaded yet.

Request
{
"jsonrpc": "2.0",
"method": "browser_currentUrl",
"params": [{ "id"?: number }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"url": string | null
},
"id": string
}

Android: 1.25.0+

browser_loadCause

Returns why the tab last loaded: "manual" (opened via browser_newTab) or "auto" (opened by a tab autonomously), or null if not yet loaded.

Request
{
"jsonrpc": "2.0",
"method": "browser_loadCause",
"params": [{ "id"?: number }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"cause": "manual" | "auto" | null
},
"id": string
}

Android: 1.25.0+

browser_startRefreshLoop

Starts a periodic refresh loop for a tab. Stop with browser_stopRefreshLoop when no longer needed.

Request
{
"jsonrpc": "2.0",
"method": "browser_startRefreshLoop",
"params": [{
"id"?: number,
"interval"?: number, // ms, default: 500
"wholeTree"?: boolean // default: false
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_stopRefreshLoop

Stops the refresh loop for a tab.

Request
{
"jsonrpc": "2.0",
"method": "browser_stopRefreshLoop",
"params": [{
"id"?: number,
"wholeTree"?: boolean // default: false
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_useProxy

Configures the WebView to route traffic through a proxy server.

Request
{
"jsonrpc": "2.0",
"method": "browser_useProxy",
"params": [{
"url": string,
"config"?: {
"username"?: string,
"password"?: string,
"fallback"?: boolean // default: false — connect directly if proxy fails
}
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_removeProxy

Removes the current proxy configuration from the WebView.

Request
{
"jsonrpc": "2.0",
"method": "browser_removeProxy",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.25.0+

browser_close

Closes all open tabs and destroys the WebView, clearing storage, cookies, and proxy settings.

Request
{
"jsonrpc": "2.0",
"method": "browser_close",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Network

Android: 1.25.0+

network_whitelist

Verifies host via DNS TXT records and, if valid, whitelists its resolved IP addresses so they bypass the rate-limiter in the network extension.

The processor performs the following verification steps:

  1. Forward DNS + TXT - verifies that _acu.<host> carries a TXT record v=base64(sha256(deployment_source || host)), then resolves the hostname to one or more IP addresses (A/AAAA).
  2. Reverse DNS + TXT - for each resolved IP, performs a PTR lookup to obtain the PTR hostname and verifies that _acu.<ptr_hostname> carries a TXT record v=base64(sha256(deployment_source || ptr_hostname)).

Both steps must pass for an IP to be added to the whitelist. Connections to non-whitelisted hosts are rate-limited.

deployment_source is the raw 32-byte Substrate Account ID of the deployment's owner. host is the bare hostname (no scheme, port, or path — e.g. example.com).

Request
{
"jsonrpc": "2.0",
"method": "network_whitelist",
"params": [{ "host": string }],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}
Verification hash

Example scripts for computing the TXT record value:

Node.js
// npm install @polkadot/util-crypto

const { createHash } = require('crypto');
const { decodeAddress } = require('@polkadot/util-crypto');

function verificationHash(source, host) {
// Decode hex Account ID directly, or convert SS58 address to raw bytes
const sourceBytes = source.length === 64
? Buffer.from(source, 'hex')
: Buffer.from(decodeAddress(source));
// SHA-256 over concatenation of source bytes and UTF-8 encoded host
const digest = createHash('sha256')
.update(sourceBytes)
.update(host)
.digest();
// Return the hash as a base64 string
return digest.toString('base64');
}
Python
# pip install substrate-interface

import hashlib
import base64

from substrateinterface.utils.ss58 import ss58_decode


def verification_hash(source: str, host: str) -> str:
# Decode hex Account ID directly, or convert SS58 address to raw bytes
source_bytes = bytes.fromhex(source) if len(source) == 64 else bytes.fromhex(ss58_decode(source))
# SHA-256 over concatenation of source bytes and UTF-8 encoded host
digest = hashlib.sha256(source_bytes + host.encode()).digest()
# Return the hash as a base64 string
return base64.b64encode(digest).decode()

Tunnel

Opens a reverse tunnel that forwards inbound TLS connections from a public URL (https://<clientId>.<domainSuffix>:8443) to a local address inside the deployment. Only one tunnel may be active per deployment.

Before calling tunnel_start, the <domainSuffix> must have the required wildcard and verification TXT records — see the tunnel quick start for details.

Android: 1.26.0+

tunnel_start

Opens the tunnel and returns the public URL it serves.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_start",
"params": [{
"serverAddrs": string[], // relay endpoints, e.g. ["relay-2.canary.acurast.com:4433"]
"domainSuffix": string, // DNS suffix you control
"localAddr": string, // local "host:port" to forward to
"primaryKey": {
"algorithm": "Secp256r1", // only P-256 is accepted
"bytes": string // base64 of PKCS#8 DER (no wrap)
},
"acmeStaging"?: boolean, // default false
"acmeEmail"?: string, // Let's Encrypt account contact
"certPem"?: string, // pre-supplied PEM chain; when set, ACME is skipped
"forceH2"?: boolean, // skip QUIC, use HTTP/2 fallback pool (default false)
"poolSize"?: number // H2 pool size when forceH2 is on (default 4)
}],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"url": string, // https://<clientId>.<domainSuffix>:8443
"clientId": string, // the deployment's tunnel identifier
"secondaryUrl"?: string,
"secondaryClientId"?: string
},
"id": string
}

Persist primaryKey.bytes across deployment restarts to keep the same clientId. Persist the cert PEM from tunnel_certPem and pass it back here as certPem to skip ACME on subsequent starts.


Android: 1.26.0+

tunnel_stop

Closes the active tunnel. The cached ACME credentials and certificate are preserved on disk so a subsequent tunnel_start with the same identity can reuse them. No-op if no tunnel is running.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_stop",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {},
"id": string
}

Android: 1.26.0+

tunnel_status

Returns the current tunnel status.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_status",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"status": number // 0 = Starting, 1 = Running, 2 = Stopped, 3 = Failed, -1 = no tunnel active
},
"id": string
}

Android: 1.26.0+

tunnel_certPem

Returns the PEM-encoded full certificate chain currently issued for the tunnel.

Request
{
"jsonrpc": "2.0",
"method": "tunnel_certPem",
"params": [],
"id": string
}
Response
{
"jsonrpc": "2.0",
"result": {
"pem"?: string // omitted if no certificate has been issued yet (or no tunnel is active)
},
"id": string
}

Examples

The examples below call processor_version to show how to connect to the bridge and make a request in different languages.

Node.js

const net = require('net');

const client = net.createConnection('\0' + process.env.BRIDGE_SOCKET);
const request = JSON.stringify({
jsonrpc: '2.0',
method: 'processor_version',
params: [],
id: '1',
});

client.write(request + '\n');
client.once('data', (data) => {
const response = JSON.parse(data.toString());
console.log(response);

client.end();
});

Python

import json, os, socket

request = json.dumps({
'jsonrpc': '2.0',
'method': 'processor_version',
'params': [],
'id': '1'
})

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect('\0' + os.environ['BRIDGE_SOCKET'])
s.sendall((request + '\n').encode())
response = json.loads(s.recv(65536))

print(response)

Rust

use std::io::{BufRead, BufReader, Write};
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixStream};

fn main() {
let socket = std::env::var("BRIDGE_SOCKET").unwrap();
let addr = SocketAddr::from_abstract_name(socket.as_bytes()).unwrap();
let mut stream = UnixStream::connect_addr(&addr).unwrap();

let request = serde_json::json!({
"jsonrpc": "2.0",
"method": "processor_version",
"params": [],
"id": "1"
}).to_string();
stream.write_all(format!("{request}\n").as_bytes()).unwrap();

let mut response = String::new();
BufReader::new(&stream).read_line(&mut response).unwrap();

let value: serde_json::Value = serde_json::from_str(&response).unwrap();
println!("{}", value);
}