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.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()

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);
}