Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/build_sandbox_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ jobs:
export PIP_DEFAULT_TIMEOUT=300
pip install -q -e ".[dev,ext]"

- name: Attempt to load Android kernel modules
Comment thread
XiuShenAl marked this conversation as resolved.
if: contains(github.event.inputs.build_types, 'mobile')
run: |
echo "1/4: Installing linux-modules-extra for kernel $(uname -r)..."
sudo apt-get update -y && sudo apt-get install -y linux-modules-extra-$(uname -r) || echo "WARNING: apt-get install failed, but continuing."
echo "2/4: Attempting to load binder_linux module..."
sudo modprobe binder_linux devices="binder,hwbinder,vndbinder" || echo "INFO: modprobe binder_linux failed."
echo "3/4: Attempting to load ashmem_linux module..."
sudo modprobe ashmem_linux || echo "INFO: modprobe ashmem_linux failed as expected."
echo "4/4: Checking loaded modules with lsmod..."
lsmod | grep -E "binder|ashmem" || echo "INFO: No 'binder' or 'ashmem' modules found in lsmod output."

- name: Run build script for all types
env:
AUTO_BUILD: "true"
Expand Down
93 changes: 7 additions & 86 deletions src/agentscope_runtime/sandbox/box/mobile/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,90 +46,6 @@ RUN mkdir -p /prod_bundle && \
cd /prod_bundle && \
npm install --production

# =================================================================
# Stage 3: Fetch Redroid Docker image (redroid-fetcher)
# =================================================================
FROM docker:28-dind AS redroid-fetcher

# NOTE:
# This stage secures the build process by pulling a third-party image (Redroid)
# using an immutable digest (SHA-256 hash) instead of a mutable tag. This prevents
# supply chain attacks where a tag could be retargeted to a malicious image.
#
# To achieve this, it starts a Docker daemon inside the build environment
# (Docker-in-Docker), pulls the specified Redroid image, and saves it as a tarball.
#
# --- DOCKER-IN-DOCKER PRIVILEGE WARNING ---
# This approach requires the build environment to support running a Docker daemon with
# sufficient privileges (e.g., privileged containers with proper cgroup access). In
# many CI/CD or restricted environments, such privileges may not be available, which
# can cause this stage to fail due to permission or daemon startup issues.
#
# If you encounter build failures at this stage, a recommended and more secure
# alternative is to perform the pull and save manually on a trusted host machine:
#
# 1. On a host with Docker access, manually pull the image using its immutable
# digest. Choose the digest corresponding to your target architecture:
#
# # For linux/amd64 (most common for servers and PCs):
# docker pull redroid/redroid@sha256:d1ca0815eb68139a43d25a835e374559e9d18f5d5cea1a4288d4657c0074fb8d
#
# # For linux/arm64 (Apple M-series, Raspberry Pi, AWS Graviton, etc.):
# docker pull redroid/redroid@sha256:f070231146ba5043bdb225a1f51c77ef0765c1157131b26cb827078bf536c922
#
# 2. Then, save the pulled image to a tarball. Use the same digest as in step 1.
# (Example for amd64):
# docker save -o redroid.tar redroid/redroid@sha256:d1ca0815eb68139a43d25a835e374559e9d18f5d5cea1a4288d4657c0074fb8d
#
# 3. Place the resulting `redroid.tar` in the Docker build context (e.g., next to
# this Dockerfile, in a path like `src/agentscope_runtime/sandbox/box/mobile/`).
#
# 4. Remove or skip this `redroid-fetcher` stage entirely, and in the final stage,
# replace the line:
# COPY --from=redroid-fetcher /redroid.tar /redroid.tar
# with a direct copy from your build context:
# COPY src/agentscope_runtime/sandbox/box/mobile/redroid.tar /redroid.tar
#
# This avoids running Docker-in-Docker and is more compatible with restricted build
# environments, while still maintaining supply chain security.

# Pin the redroid image to an immutable digest for security and reproducibility.
# The default digest is for the linux/amd64 architecture.
# To build for linux/arm64, pass the --build-arg flag to the docker build command:
# --build-arg REDROID_DIGEST=sha256:f070231146ba5043bdb225a1f51c77ef0765c1157131b26cb827078bf536c922
ARG REDROID_DIGEST=sha256:d1ca0815eb68139a43d25a835e374559e9d18f5d5cea1a4288d4657c0074fb8d

# --- Display a warning to the user before the privileged operation ---
RUN echo "" && \
echo "========================================================================" && \
echo " >>> WARNING: Privileged Operation Ahead <<<" && \
echo "========================================================================" && \
echo "The following step will start a Docker-in-Docker (DinD) daemon." && \
echo "This operation requires high privileges (e.g., the --privileged flag)" && \
echo "and may fail in restricted environments like CI/CD pipelines." && \
echo "" && \
echo " --- IF THIS STEP FAILS, USE THE ALTERNATIVE BELOW ---" && \
echo "Manually 'docker pull' and 'docker save' the image to a .tar file, then" && \
echo "copy it into the build context. For detailed instructions, please refer" && \
echo "to the comments at the top of this stage in the Dockerfile:" && \
echo " src/agentscope_runtime/sandbox/box/mobile/Dockerfile" && \
echo "========================================================================" && \
echo ""

# --- Run the Docker-in-Docker process ---
RUN dockerd-entrypoint.sh & \
TIMEOUT=30; \
while ! docker info > /dev/null 2>&1; do \
if [ $TIMEOUT -le 0 ]; then \
echo "Docker daemon did not become ready in time." >&2; \
exit 1; \
fi; \
sleep 1; \
TIMEOUT=$((TIMEOUT - 1)); \
done && \
docker pull redroid/redroid@${REDROID_DIGEST} && \
docker save -o /redroid.tar redroid/redroid@${REDROID_DIGEST}

# =================================================================
# Final Stage: Production Image
# =================================================================
Expand Down Expand Up @@ -167,8 +83,13 @@ COPY src/agentscope_runtime/sandbox/box/mobile/box/mcp_server_configs.json /app/
COPY src/agentscope_runtime/sandbox/box/mobile/box/scripts/start.sh /start.sh
RUN chmod +x /start.sh

# 6. Copy the offline redroid image from the fetcher stage
COPY --from=redroid-fetcher /redroid.tar /redroid.tar
RUN echo "[BUILD NOTE] This step requires:" && \
echo " 'src/agentscope_runtime/sandbox/box/mobile/redroid.tar'." && \
echo "This file is generated by the build script ('build.py')." && \
echo "If building manually, please prepare the necessary files."
# 6. Copy the offline redroid image prepared by the build script
# NOTE: The build.py script is responsible for creating this file in the build context.
COPY src/agentscope_runtime/sandbox/box/mobile/redroid.tar /redroid.tar

# 7. Set entrypoint
ENTRYPOINT ["/start.sh"]
112 changes: 112 additions & 0 deletions src/agentscope_runtime/sandbox/box/mobile/box/host_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
import platform
import subprocess
import logging


class HostPrerequisiteError(Exception):
"""Exception raised when host prerequisites
for MobileSandbox are not met."""


logger = logging.getLogger(__name__)


def check_mobile_sandbox_host_readiness() -> None:
"""
Performs a check of the host environment to ensure it has the necessary
modules (like binder_linux) to run the MobileSandbox.
"""
Comment thread
XiuShenAl marked this conversation as resolved.
logger.info(
"Performing host environment check for MobileSandbox readiness...",
)

architecture = platform.machine().lower()
if architecture in ("aarch64", "arm64"):
logger.warning(
"\n======================== WARNING ========================\n"
"ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
"Running this mobile sandbox on a non-x86_64 host may lead \n"
" to unexpected compatibility or performance issues.\n"
"=========================================================",
)

os_type = platform.system()
if os_type == "Linux":
try:
result = subprocess.run(
["lsmod"],
capture_output=True,
text=True,
check=True,
)
loaded_modules = result.stdout
except (FileNotFoundError, subprocess.CalledProcessError):
loaded_modules = ""
logger.warning(
"Could not execute 'lsmod' to verify kernel modules.",
)

if "binder_linux" not in loaded_modules:
error_message = (
"\n========== HOST PREREQUISITE FAILED ==========\n"
"MobileSandbox requires specific kernel modules"
" that appear to be missing or not loaded.\n\n"
"To fix this, please run the following commands"
" on your Linux host:\n\n"
"## Install required kernel modules\n"
"sudo apt update"
" && sudo apt install -y linux-modules-extra-`uname -r`\n"
"sudo modprobe binder_linux"
' devices="binder,hwbinder,vndbinder"\n'
"## (Optional) Load the ashmem driver for older kernels\n"
"sudo modprobe ashmem_linux\n"
"=================================================="
)
raise HostPrerequisiteError(error_message)

if os_type == "Windows":
try:
result = subprocess.run(
["wsl", "lsmod"],
capture_output=True,
text=True,
check=True,
encoding="utf-8",
)
loaded_modules = result.stdout
except (FileNotFoundError, subprocess.CalledProcessError):
loaded_modules = ""
logger.warning(
"Could not execute 'wsl lsmod' to verify kernel modules.",
)

if "binder_linux" not in loaded_modules:
error_message = (
"\n========== HOST PREREQUISITE FAILED ==========\n"
"MobileSandbox on Windows requires Docker Desktop "
"with the WSL 2 backend.\n"
"The required kernel modules seem to be missing "
"in your WSL 2 environment.\n\n"
"To fix this, please follow these steps:\n\n"
"1. **Ensure Docker Desktop is using WSL 2**:\n"
" - Open Docker Desktop -> Settings -> General.\n"
" - Make sure 'Use the WSL 2 based engine' "
"is checked.\n\n"
"2. **Ensure WSL is installed and updated**:\n"
" - Open PowerShell or Command Prompt "
"as Administrator.\n"
" - Run: wsl --install\n"
" - Run: wsl --update\n"
" (An update usually installs a recent Linux kernel "
"with the required modules.)\n\n"
"3. **Verify manually (Optional)**:\n"
" - After updating, run 'wsl lsmod | findstr binder' "
"in your terminal.\n"
" - If it shows 'binder_linux', "
"the issue should be resolved.\n"
"=================================================="
)
raise HostPrerequisiteError(error_message)

logger.info("Host environment check passed.")
36 changes: 32 additions & 4 deletions src/agentscope_runtime/sandbox/box/mobile/box/scripts/start.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
#!/bin/sh
set -e

RUN_MODE="NORMAL"
LOCK_FILE="/var/run/agentscope_first_run.lock"

if [ "$BUILT_BY_SCRIPT" = "true" ]; then
echo "--> 'BUILT_BY_SCRIPT' flag detected. Activating advanced run-mode detection."
if [ ! -f "$LOCK_FILE" ]; then
RUN_MODE="HEALTH_CHECK"
echo "--> First run under build script detected (Health Check Mode). Creating lock file..."
mkdir -p "$(dirname "$LOCK_FILE")"
touch "$LOCK_FILE"
Comment thread
rayrayraykk marked this conversation as resolved.
else
RUN_MODE="NORMAL"
echo "--> Subsequent run under build script detected (Normal Mode)."
fi
else
echo "--> 'BUILT_BY_SCRIPT' flag not found. Assuming standard Normal Mode."
RUN_MODE="NORMAL"
fi

echo "--- Phase 1: Starting internal Docker Daemon ---"
dockerd-entrypoint.sh &
dockerd_pid=$!
Expand All @@ -11,14 +30,19 @@ done
echo "--> Internal Docker Daemon is UP!"

echo "--- Phase 2: Loading and starting nested Redroid container ---"
REDROID_IMAGE="redroid/redroid:11.0.0-240527"
REDROID_IMAGE="agentscope/redroid:internal"

if [ -z "$(docker images -q "$REDROID_IMAGE")" ]; then
if [ -f /redroid.tar ]; then
echo "--> Loading Redroid image from /redroid.tar..."
docker load -i /redroid.tar
echo "--> Successfully loaded Redroid image."
rm /redroid.tar
if [ "$RUN_MODE" = "NORMAL" ]; then
echo "--> Normal mode: Removing /redroid.tar."
rm /redroid.tar
else # RUN_MODE is "HEALTH_CHECK"
echo "--> Health check mode: Preserving /redroid.tar for commit."
fi
else
echo "[FATAL ERROR] Built-in /redroid.tar not found!"
exit 1
Expand All @@ -27,6 +51,11 @@ else
echo "--> Redroid image already exists."
fi

if [ -z "$(docker images -q "$REDROID_IMAGE")" ]; then
echo "[FATAL ERROR] Failed to load Redroid image '$REDROID_IMAGE' from tarball."
exit 1
fi

if [ "$(docker ps -q -f name=redroid_nested)" ]; then
echo "Nested redroid container is already running."
else
Expand Down Expand Up @@ -105,5 +134,4 @@ echo "--> Nginx & FastAPI & WS-Scrcpy services started."
supervisorctl status

echo "--> Orchestration complete. System is fully operational."
wait $dockerd_pid

wait $dockerd_pid
Loading