Giving Zero-Assist Its Own Linux Sandbox
A lot of features I want in Zero-Assist need actual Linux tools: shell commands, Python, Git, Node.js, CLI tools, etc. I didn’t want this to require root, so I built a local Alpine Linux sandbox using PRoot that runs entirely on-device.
The architecture is basically:
AI daemon → sandbox tool → HTTP bridge → Kotlin sandbox manager → PRoot → Alpine
Getting PRoot running
One annoying problem was Android’s noexec restrictions. PRoot is stored inside the app’s data/native library directory and can’t always be executed normally.
The solution was launching it through Android’s dynamic linker:
private fun buildProcessArgs(
command: String,
workingDir: String
): Array<String> {
val args = arrayOf(
prootPath,
"--rootfs=$rootfsPath",
"--bind=/dev",
"--bind=/proc",
"--bind=/sys",
"--bind=$homePath:/root",
"--bind=$tmpPath:/tmp",
"-0", "-w", workingDir,
"/bin/sh", "-c", command
)
return if (ANDROID_LINKER != null)
arrayOf(ANDROID_LINKER, *args)
else args
}
So commands effectively go through:
/system/bin/linker64 → PRoot → Alpine → command
This lets the bundled PRoot code run despite the noexec restriction.
Connecting Rust and Kotlin
My AI daemon is written in Rust, while the sandbox is managed by Kotlin. I connected them using a small NanoHTTPD server running only on localhost.
return when (session.uri) {
"/health" -> handleHealth()
"/execute" -> handleExecute(session)
"/manage_process" -> handleManageProcess(session)
else -> errorJson(
Response.Status.NOT_FOUND,
"Unknown path: ${session.uri}"
)
}
The Rust daemon sends authenticated requests to 127.0.0.1:49481.
The AI currently gets two tools:
-
sandbox_execute- run Linux commands -
sandbox_manage_process- manage background processes
Persistent shell sessions
I wanted the sandbox to behave like an actual terminal instead of starting from scratch for every command.
If the AI runs:
cd /root/project
export TEST=hello
the next tool call stays in /root/project and keeps the environment variable.
Each conversation gets a separate persistent shell:
Conversation A → Shell A
Conversation B → Shell B
This keeps cwd/environment state while preventing different conversations from affecting each other.
Background commands are isolated differently. Each one gets a separate PRoot executor and is tracked by a process manager. The AI can start a long-running command, continue doing other things, then check its logs or kill it later.
Installing Alpine
The sandbox manager handles the full setup:
Download Alpine → Extract → Fix hardlinks/permissions → apk update → Install tools → Ready
The hardlink fix was needed because some Alpine tar entries reference files that haven’t been extracted yet, which caused extraction failures on Android.
Right now the sandbox comes with Bash, Python + pip, Node.js, Git, curl, wget, jq, SSH, lftp and rsync.
The main parts are:
-
ProotExecutor- executes commands -
LinuxSandboxManager- lifecycle/setup -
SandboxBridgeServer- Rust ↔ Kotlin bridge -
SandboxProcessManager- background processes -
PersistentSandboxShell- per-conversation shell state
There is still more I want to improve, especially filesystem sharing, networking and reliability for longer agent tasks.
But the main system is finally working: Zero-Assist can now give an AI agent a real Linux userspace running locally inside an Android app without requiring root.
This started with the idea of somehow giving the AI a tiny Linux computer inside the app. Now it actually has one.
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.