diff --git a/.github/workflows/build_alias_sandbox_image.yml b/.github/workflows/build_alias_sandbox_image.yml new file mode 100644 index 0000000..1b3bcbc --- /dev/null +++ b/.github/workflows/build_alias_sandbox_image.yml @@ -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 diff --git a/alias/src/alias/runtime/alias_sandbox/box/routers/generic.py b/alias/src/alias/runtime/alias_sandbox/box/routers/generic.py index eeea6a5..e9b71ed 100644 --- a/alias/src/alias/runtime/alias_sandbox/box/routers/generic.py +++ b/alias/src/alias/runtime/alias_sandbox/box/routers/generic.py @@ -2,7 +2,7 @@ import io import sys import logging -import subprocess +import asyncio import traceback from contextlib import redirect_stderr, redirect_stdout @@ -44,26 +44,33 @@ async def run_ipython_cell( stdout_buf = io.StringIO() stderr_buf = io.StringIO() - with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf): - preprocessing_exc_tuple = None - try: - transformed_cell = ipy.transform_cell(code) - except Exception: - transformed_cell = code - preprocessing_exc_tuple = sys.exc_info() + def thread_target(): + with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf): + preprocessing_exc_tuple = None + try: + transformed_cell = ipy.transform_cell(code) + except Exception: + transformed_cell = code + preprocessing_exc_tuple = sys.exc_info() - if transformed_cell is None: - raise HTTPException( - status_code=500, - detail="IPython cell transformation failed: " - "transformed_cell is None.", + if transformed_cell is None: + raise HTTPException( + status_code=500, + detail=( + "IPython cell transformation failed: " + "transformed_cell is None." + ), + ) + + asyncio.run( + ipy.run_cell_async( + code, + transformed_cell=transformed_cell, + preprocessing_exc_tuple=preprocessing_exc_tuple, + ), ) - await 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", ), ) diff --git a/alias/src/alias/runtime/alias_sandbox/box/vnc_relay.html b/alias/src/alias/runtime/alias_sandbox/box/vnc_relay.html index 386f9f5..c25baab 100644 --- a/alias/src/alias/runtime/alias_sandbox/box/vnc_relay.html +++ b/alias/src/alias/runtime/alias_sandbox/box/vnc_relay.html @@ -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);