You are browsing as a guest. Sign up (or log in) to start making projects!

10h 26m 52s logged

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.

0
2

Comments 0

No comments yet. Be the first!