feature: enhance alias sandbox performance when running code (#91)

This commit is contained in:
Weirui Kuang
2026-01-07 11:09:44 +08:00
committed by GitHub
parent 1f0cd9b58a
commit fe4b7f53b0
3 changed files with 265 additions and 31 deletions

View File

@@ -0,0 +1,208 @@
name: Manual Build Alias Sandbox Image
on:
workflow_dispatch:
inputs:
platform:
description: "Docker architecture platform (used for multi-arch builds)"
required: true
default: "linux/amd64"
type: choice
options:
- linux/amd64
- linux/arm64
single_arch:
description: "Single architecture build (forces amd64, no multi-arch manifest merge)"
required: true
default: "false"
type: choice
options:
- "true"
- "false"
tag:
description: "Custom image tag (default: current date)"
required: false
default: ""
type: string
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest ]
python-version: [ '3.10' ]
environment: prod
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }}
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Log in to Aliyun ACR
env:
ALIYUN_ACR_USERNAME: ${{ secrets.ALIYUN_ACR_USERNAME }}
ALIYUN_ACR_PASSWORD: ${{ secrets.ALIYUN_ACR_PASSWORD }}
if: ${{ env.ALIYUN_ACR_USERNAME != '' && env.ALIYUN_ACR_PASSWORD != '' }}
uses: docker/login-action@v3
with:
registry: agentscope-registry.ap-southeast-1.cr.aliyuncs.com
username: ${{ env.ALIYUN_ACR_USERNAME }}
password: ${{ env.ALIYUN_ACR_PASSWORD }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Update setuptools
run: |
pip install --upgrade pip
pip install setuptools==78.1.1 wheel==0.45.1
- name: Set PYTHONPATH
run: |
echo "PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/src" >> $GITHUB_ENV
- name: Install dependencies
run: |
export PIP_DEFAULT_TIMEOUT=300
pip install agentscope-runtime loguru
- name: Run build script for all types
working-directory: alias/src/alias/runtime/alias_sandbox
env:
AUTO_BUILD: "true"
run: |
TAG_INPUT="${{ github.event.inputs.tag }}"
if [ -z "$TAG_INPUT" ]; then
TAG_INPUT=$(date +%Y%m%d)
fi
PLATFORM_INPUT="${{ github.event.inputs.platform }}"
SINGLE="${{ github.event.inputs.single_arch }}"
if [ "$SINGLE" = "true" ]; then
PLATFORM_INPUT="linux/amd64"
fi
IFS=',' read -ra TYPES <<< "alias"
for TYPE in "${TYPES[@]}"; do
runtime-sandbox-builder "$TYPE" --platform "$PLATFORM_INPUT" --dockerfile_path Dockerfile --extension alias_sandbox.py
done
- name: Tag & Push Images
working-directory: alias/src/alias/runtime/alias_sandbox
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
ALIYUN_ACR_USERNAME: ${{ secrets.ALIYUN_ACR_USERNAME }}
ALIYUN_ACR_PASSWORD: ${{ secrets.ALIYUN_ACR_PASSWORD }}
run: |
TAG_INPUT="${{ github.event.inputs.tag }}"
if [ -z "$TAG_INPUT" ]; then
TAG_INPUT=$(date +%Y%m%d)
fi
SINGLE="${{ github.event.inputs.single_arch }}"
ARCH_TAG="${{ github.event.inputs.platform }}"
if [ "$SINGLE" = "true" ]; then
ARCH_TAG="linux/amd64"
fi
IFS=',' read -ra TYPES <<< "alias"
for TYPE in "${TYPES[@]}"; do
IMAGE_BASE=$(python -c "import alias_sandbox; from agentscope_runtime.sandbox.registry import SandboxRegistry; print(SandboxRegistry.get_image_by_type('$TYPE'))")
IMAGE_NAME="${IMAGE_BASE%%:*}"
if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then
if [ "$SINGLE" = "true" ]; then
docker tag "$IMAGE_BASE" "docker.io/${IMAGE_NAME}:${TAG_INPUT}"
docker push "docker.io/${IMAGE_NAME}:${TAG_INPUT}"
else
ARCH_SUFFIX=$(echo "$ARCH_TAG" | tr '/' '-')
docker tag "$IMAGE_BASE" "docker.io/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
docker push "docker.io/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
fi
fi
if [ -n "$ALIYUN_ACR_USERNAME" ] && [ -n "$ALIYUN_ACR_PASSWORD" ]; then
REGISTRY="agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
if [ "$SINGLE" = "true" ]; then
docker tag "$IMAGE_BASE" "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}"
docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}"
else
ARCH_SUFFIX=$(echo "$ARCH_TAG" | tr '/' '-')
docker tag "$IMAGE_BASE" "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
fi
fi
done
- name: Create Multi-arch Manifest for DockerHub
working-directory: alias/src/alias/runtime/alias_sandbox
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
if: ${{ github.event.inputs.single_arch == 'false' && env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }}
run: |
TAG_INPUT="${{ github.event.inputs.tag }}"
if [ -z "$TAG_INPUT" ]; then
TAG_INPUT=$(date +%Y%m%d)
fi
IFS=',' read -ra TYPES <<< "alias"
for TYPE in "${TYPES[@]}"; do
IMAGE_BASE=$(python -c "import alias_sandbox; from agentscope_runtime.sandbox.registry import SandboxRegistry; print(SandboxRegistry.get_image_by_type('$TYPE'))")
IMAGE_NAME="${IMAGE_BASE%%:*}"
AMD_TAG="docker.io/${IMAGE_NAME}:${TAG_INPUT}-linux-amd64"
ARM_TAG="docker.io/${IMAGE_NAME}:${TAG_INPUT}-linux-arm64"
COMMON_TAG="docker.io/${IMAGE_NAME}:${TAG_INPUT}"
if docker manifest inspect "$AMD_TAG" >/dev/null 2>&1 && docker manifest inspect "$ARM_TAG" >/dev/null 2>&1; then
docker manifest create "$COMMON_TAG" --amend "$AMD_TAG" --amend "$ARM_TAG"
docker manifest push "$COMMON_TAG"
else
echo "Missing architecture image in DockerHub, skipping manifest creation."
fi
done
- name: Create Multi-arch Manifest for Aliyun ACR
working-directory: alias/src/alias/runtime/alias_sandbox
env:
ALIYUN_ACR_USERNAME: ${{ secrets.ALIYUN_ACR_USERNAME }}
ALIYUN_ACR_PASSWORD: ${{ secrets.ALIYUN_ACR_PASSWORD }}
if: ${{ github.event.inputs.single_arch == 'false' && env.ALIYUN_ACR_USERNAME != '' && env.ALIYUN_ACR_PASSWORD != '' }}
run: |
TAG_INPUT="${{ github.event.inputs.tag }}"
if [ -z "$TAG_INPUT" ]; then
TAG_INPUT=$(date +%Y%m%d)
fi
REG="agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
IFS=',' read -ra TYPES <<< "alias"
for TYPE in "${TYPES[@]}"; do
IMAGE_BASE=$(python -c "import alias_sandbox; from agentscope_runtime.sandbox.registry import SandboxRegistry; print(SandboxRegistry.get_image_by_type('$TYPE'))")
IMAGE_NAME="${IMAGE_BASE%%:*}"
AMD_TAG="${REG}/${IMAGE_NAME}:${TAG_INPUT}-linux-amd64"
ARM_TAG="${REG}/${IMAGE_NAME}:${TAG_INPUT}-linux-arm64"
COMMON_TAG="${REG}/${IMAGE_NAME}:${TAG_INPUT}"
if docker manifest inspect "$AMD_TAG" >/dev/null 2>&1 && docker manifest inspect "$ARM_TAG" >/dev/null 2>&1; then
docker manifest create "$COMMON_TAG" --amend "$AMD_TAG" --amend "$ARM_TAG"
docker manifest push "$COMMON_TAG"
else
echo "Missing architecture image in Aliyun ACR, skipping manifest creation."
fi
done

View File

@@ -2,7 +2,7 @@
import io
import sys
import logging
import subprocess
import asyncio
import traceback
from contextlib import redirect_stderr, redirect_stdout
@@ -44,6 +44,7 @@ async def run_ipython_cell(
stdout_buf = io.StringIO()
stderr_buf = io.StringIO()
def thread_target():
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
preprocessing_exc_tuple = None
try:
@@ -55,16 +56,22 @@ async def run_ipython_cell(
if transformed_cell is None:
raise HTTPException(
status_code=500,
detail="IPython cell transformation failed: "
"transformed_cell is None.",
detail=(
"IPython cell transformation failed: "
"transformed_cell is None."
),
)
await ipy.run_cell_async(
asyncio.run(
ipy.run_cell_async(
code,
transformed_cell=transformed_cell,
preprocessing_exc_tuple=preprocessing_exc_tuple,
),
)
await asyncio.to_thread(thread_target)
stdout_content = stdout_buf.getvalue()
stderr_content = stderr_buf.getvalue()
@@ -128,16 +135,16 @@ async def run_shell_command(
if not command:
raise HTTPException(status_code=400, detail="Command is required.")
result = subprocess.run(
proc = await asyncio.create_subprocess_shell(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout_content = result.stdout
stderr_content = result.stderr
stdout_bytes, stderr_bytes = await proc.communicate()
stdout_content = stdout_bytes.decode()
stderr_content = stderr_bytes.decode()
content_list = []
@@ -161,7 +168,7 @@ async def run_shell_command(
content_list.append(
TextContent(
type="text",
text=str(result.returncode),
text=str(proc.returncode),
description="returncode",
),
)
@@ -173,7 +180,7 @@ async def run_shell_command(
+ "\n"
+ stderr_content
+ "\n"
+ str(result.returncode),
+ str(proc.returncode),
description="output",
),
)

View File

@@ -107,12 +107,31 @@
function getSandboxIdFromPath() {
const pathParts = window.location.pathname.split('/');
if (pathParts.length >= 3 && pathParts[1] === 'desktop') {
return pathParts[2];
// Find 'desktop' anywhere in the path to support arbitrary URL prefixes
// Supports: /desktop/{sandbox_id}/... or /custom/prefix/desktop/{sandbox_id}/...
const desktopIndex = pathParts.indexOf('desktop');
if (desktopIndex !== -1 && pathParts.length > desktopIndex + 1) {
// Return null if sandbox_id is empty (e.g., /desktop/ or /desktop//)
return pathParts[desktopIndex + 1] || null;
}
return null;
}
function getWebSocketPath(sandbox_id) {
// Extract the path prefix up to and including /desktop/{sandbox_id}
// This preserves any custom URL prefix for reverse proxy deployments
const currentPath = window.location.pathname;
// Use non-greedy matching (.*?) to find the FIRST occurrence of /desktop/
// This ensures consistency with getSandboxIdFromPath() which uses indexOf()
const desktopMatch = currentPath.match(/(.*?)\/desktop\/([^\/]+)/);
if (desktopMatch) {
// Use the passed sandbox_id parameter for consistency
return desktopMatch[1] + '/desktop/' + sandbox_id;
}
// Fallback to simple path
return '/desktop/' + sandbox_id;
}
document.getElementById('sendCtrlAltDelButton')
.onclick = sendCtrlAltDel;
@@ -141,7 +160,7 @@
url += ':' + port;
}
url += '/desktop/' + sandbox_id;
url += getWebSocketPath(sandbox_id);
if (path && path !== 'websockify') {
url += '?path=' + encodeURIComponent(path);