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
8 changes: 8 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,14 @@ The Studio `环境` page stores each environment definition, generated Dockerfil
build version, log metadata, and resulting image reference in the private Studio
TOS bucket. Creating or saving an environment starts an asynchronous
CodePipeline build and pushes the resulting image to Container Registry.
New environments default to the AIO Sandbox base image, which keeps the inherited
`/opt/gem/run.sh` entrypoint and port `8080` so the Sandbox Shell API remains
available. Standard Ubuntu 22.04 and 24.04 images remain supported, and records
created before the base-environment field was introduced resolve to Ubuntu.
Each image version exposes a read-only Manifest at
`/web/environments/{environmentId}/builds/{versionId}/manifest`; the Studio
environment card opens the same version-bound contract as YAML for inspection
and copying.
Volcengine builds use the Aliyun PyPI mirror, Huawei Cloud Python source mirror,
and npmmirror for Playwright browsers; BytePlus builds use the corresponding
official sources. Cross-version Python combinations are compiled from pinned
Expand Down
106 changes: 106 additions & 0 deletions frontend/server/environments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,69 @@
from collections.abc import Callable, Mapping
from typing import Any

from frontend.server.agentkit_clients import create_agentkit_client
from frontend.server.storage import StudioProvider, StudioStorageConfig
from frontend.server.storage.tos import CredentialResolver, create_tos_client_factory
from veadk.auth.veauth.ark_veauth import get_ark_token
from veadk.cli.agentkit_sandbox_region import resolve_sandbox_client_region
from veadk.cli.studio_model_catalog import modelark_base_url, studio_agent_model_name
from veadk.utils.cloud_provider import default_region

from .repository import TosEnvironmentRepository
from .resources import EnvironmentResourceSettings, StudioEnvironmentCloudGateway
from .routes import mount_environment_routes
from .service import EnvironmentService, WorkspaceReferenceLookup
from .tool_provisioning import AgentkitEnvironmentToolProvisioner


def _environment_tool_model_env(
*,
provider: StudioProvider,
region: str,
source: Mapping[str, str],
resolve_credentials: CredentialResolver,
) -> Mapping[str, str]:
access_key, secret_key, session_token = resolve_credentials()
model_api_key = str(
source.get("MODEL_AGENT_API_KEY")
or source.get("CODEX_API_KEY")
or get_ark_token(
region=resolve_sandbox_client_region(region, provider=provider),
api_key_name=(
str(source.get("MODEL_AGENT_API_KEY_NAME") or "").strip() or None
),
cloud_provider=provider,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
)
).strip()
model_name = str(
source.get("MODEL_AGENT_NAME")
or source.get("CODEX_MODEL")
or studio_agent_model_name(provider)
).strip()
base_url = (
str(
source.get("MODEL_AGENT_BASE_URL")
or source.get("MODEL_AGENT_API_BASE")
or source.get("CODEX_BASE_URL")
or modelark_base_url(provider)
)
.strip()
.rstrip("/")
)
model_provider = str(source.get("MODEL_AGENT_PROVIDER") or "openai").strip()
return {
"MODEL_AGENT_API_KEY": model_api_key,
"MODEL_AGENT_NAME": model_name,
"MODEL_AGENT_API_BASE": base_url,
"MODEL_AGENT_BASE_URL": base_url,
"MODEL_AGENT_PROVIDER": model_provider,
"CODEX_API_KEY": model_api_key,
"CODEX_BASE_URL": base_url,
"CODEX_MODEL": model_name,
}


def create_environment_service(
Expand Down Expand Up @@ -63,6 +118,35 @@ def create_environment_service(
bucket=storage.bucket,
source=source,
)
tool_provisioner = None
if resolve_credentials is not None:

def _tool_client(tool_provider: str, region: str) -> Any:
if tool_provider != provider:
raise ValueError("环境 Tool 的云服务商与当前 Studio 不一致。")
return _create_environment_tools_client(
provider,
region,
resolve_credentials,
)

def _tool_model_environment(
tool_provider: str,
region: str,
) -> Mapping[str, str]:
if tool_provider != provider:
raise ValueError("环境 Tool 的云服务商与当前 Studio 不一致。")
return _environment_tool_model_env(
provider=provider,
region=region,
source=source,
resolve_credentials=resolve_credentials,
)

tool_provisioner = AgentkitEnvironmentToolProvisioner(
_tool_client,
model_environment_resolver=_tool_model_environment,
)
return EnvironmentService(
TosEnvironmentRepository(
bucket=storage.bucket,
Expand All @@ -73,7 +157,29 @@ def create_environment_service(
resolve_credentials=resolve_credentials,
),
workspace_references=workspace_references,
tool_provisioner=tool_provisioner,
)


def _create_environment_tools_client(
provider: StudioProvider,
region: str,
resolve_credentials: CredentialResolver,
) -> Any:
from agentkit.sdk.tools.client import AgentkitToolsClient

access_key, secret_key, session_token = resolve_credentials()
client = create_agentkit_client(
AgentkitToolsClient,
provider=provider,
access_key=access_key,
secret_key=secret_key,
region=resolve_sandbox_client_region(region, provider=provider),
session_token=session_token or "",
)
if provider != "byteplus":
client.set_host("open.volcengineapi.com")
return client


__all__ = [
Expand Down
139 changes: 101 additions & 38 deletions frontend/server/environments/dockerfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class _Package:
"ubuntu-24.04": ("Ubuntu 24.04", "ubuntu:24.04"),
}

AIO_BASE_IMAGE = (
"agentkit-cli-2107625663-cn-beijing.cr.volces.com/agentkit/"
"agent-native-requirements-aio:0.2.1-20260831"
)

_PYTHON_PATCH_VERSIONS = {
"3.10": "3.10.18",
"3.12": "3.12.11",
Expand Down Expand Up @@ -156,11 +161,17 @@ def _extend_unique(target: list[str], packages: tuple[str, ...] | list[str]) ->


def _apt_packages(
config: EnvironmentInput, *, python_version: str, uses_ubuntu_python: bool
config: EnvironmentInput,
*,
python_version: str,
uses_ubuntu_python: bool,
uses_aio_python: bool,
) -> list[str]:
"""Collect every system package so the generated image needs one apt update."""
packages = ["ca-certificates"]
if uses_ubuntu_python:
if uses_aio_python:
pass
elif uses_ubuntu_python:
_extend_unique(
packages,
[f"python{python_version}", f"python{python_version}-venv"],
Expand Down Expand Up @@ -191,7 +202,8 @@ def build_dockerfile(config: EnvironmentInput) -> str:
if config.dockerfile:
return validate_dockerfile(config.dockerfile)

os_label, base_image = _OPERATING_SYSTEMS[config.operating_system]
os_label, ubuntu_base_image = _OPERATING_SYSTEMS[config.operating_system]
uses_aio_python = config.base_environment == "aio-sandbox"
python_version = config.language.removeprefix("python-")
python_patch_version = _PYTHON_PATCH_VERSIONS[python_version]
selected_options = set(config.option_ids)
Expand All @@ -202,39 +214,62 @@ def build_dockerfile(config: EnvironmentInput) -> str:
config,
python_version=python_version,
uses_ubuntu_python=uses_ubuntu_python,
uses_aio_python=uses_aio_python,
)
lines = (
[
f"ARG AIO_BASE_IMAGE={AIO_BASE_IMAGE}",
"ARG AIO_BASE_PLATFORM=linux/amd64",
"",
f"# Base environment: AIO Sandbox ({os_label})",
"FROM --platform=${AIO_BASE_PLATFORM} ${AIO_BASE_IMAGE}",
]
if uses_aio_python
else [f"# Operating system: {os_label}", f"FROM {ubuntu_base_image}"]
)
lines = [
f"# Operating system: {os_label}",
f"FROM {base_image}",
"",
"ARG DEBIAN_FRONTEND=noninteractive",
"ARG APT_MIRROR_URL=http://archive.ubuntu.com/ubuntu",
"ARG PIP_INDEX_URL=https://pypi.org/simple",
"ARG PYTHON_SOURCE_BASE_URL=https://www.python.org/ftp/python",
"ARG PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.playwright.dev",
"ARG PIP_DEFAULT_TIMEOUT=300",
"ARG PIP_RETRIES=10",
"",
"# Install all system dependencies in one transaction from the provider-local mirror.",
"RUN set -eux; \\",
' mirror="${APT_MIRROR_URL%/}"; \\',
" for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do \\",
' [ -f "$source_file" ] || continue; \\',
' sed -i -E "s#https?://(archive|security).ubuntu.com/ubuntu/?#${mirror}#g" "$source_file"; \\',
" done; \\",
' printf \'Acquire::Retries "5";\\nAcquire::ForceIPv4 "true";\\nAcquire::http::Timeout "60";\\nAcquire::https::Timeout "60";\\n\' > /etc/apt/apt.conf.d/80-veadk-network; \\',
" apt-get update; \\",
" apt-get install -y --no-install-recommends \\",
*(f" {package} \\" for package in apt_packages),
" ; rm -rf /var/lib/apt/lists/*",
"",
"ENV PYTHONDONTWRITEBYTECODE=1 \\",
" PYTHONUNBUFFERED=1 \\",
" PIP_NO_CACHE_DIR=1",
"",
f"# Python {python_version}",
]
if uses_ubuntu_python:
lines.extend(
[
"",
"ARG DEBIAN_FRONTEND=noninteractive",
"ARG APT_MIRROR_URL=http://archive.ubuntu.com/ubuntu",
"ARG PIP_INDEX_URL=https://pypi.org/simple",
"ARG PYTHON_SOURCE_BASE_URL=https://www.python.org/ftp/python",
"ARG PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.playwright.dev",
"ARG PIP_DEFAULT_TIMEOUT=300",
"ARG PIP_RETRIES=10",
"",
"# Install all system dependencies in one transaction from the provider-local mirror.",
"RUN set -eux; \\",
' mirror="${APT_MIRROR_URL%/}"; \\',
" for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do \\",
' [ -f "$source_file" ] || continue; \\',
' sed -i -E "s#https?://(archive|security).ubuntu.com/ubuntu/?#${mirror}#g" "$source_file"; \\',
" done; \\",
' printf \'Acquire::Retries "5";\\nAcquire::ForceIPv4 "true";\\nAcquire::http::Timeout "60";\\nAcquire::https::Timeout "60";\\n\' > /etc/apt/apt.conf.d/80-veadk-network; \\',
" apt-get update; \\",
" apt-get install -y --no-install-recommends \\",
*(f" {package} \\" for package in apt_packages),
" ; rm -rf /var/lib/apt/lists/*",
"",
"ENV PYTHONDONTWRITEBYTECODE=1 \\",
" PYTHONUNBUFFERED=1 \\",
" PIP_NO_CACHE_DIR=1",
"",
f"# Python {python_version}",
]
)
if uses_aio_python:
lines.extend(
(
"# Keep Studio dependencies isolated from AIO's system interpreter.",
"RUN /opt/python3.12/bin/python -m venv /opt/veadk-environment/.venv",
"",
"ENV VIRTUAL_ENV=/opt/veadk-environment/.venv \\",
" BASH_VENV_PATH=/opt/veadk-environment/.venv \\",
' PATH="/opt/veadk-environment/.venv/bin:$PATH"',
)
)
elif uses_ubuntu_python:
lines.append(f"RUN python{python_version} -m venv /opt/venv")
else:
lines.extend(
Expand All @@ -250,7 +285,8 @@ def build_dockerfile(config: EnvironmentInput) -> str:
" && rm -rf /tmp/python-source /tmp/python.tgz",
)
)
lines.extend(("", 'ENV PATH="/opt/venv/bin:$PATH"'))
if not uses_aio_python:
lines.extend(("", 'ENV PATH="/opt/venv/bin:$PATH"'))
if {"playwright", "chromium"} & selected_options:
lines.extend(
(
Expand Down Expand Up @@ -298,10 +334,31 @@ def build_dockerfile(config: EnvironmentInput) -> str:
browser_installed = True
elif package.installer != "apt":
lines.append(f"RUN python -m pip install --upgrade {package.package_name}")
lines.extend(("", 'CMD ["/bin/bash"]'))
if uses_aio_python:
lines.extend(
(
"",
"# Keep AIO's inherited /opt/gem/run.sh startup chain and shell API.",
"EXPOSE 8080",
)
)
else:
lines.extend(("", 'CMD ["/bin/bash"]'))
return "\n".join(lines)


def environment_base_image(config: EnvironmentInput) -> str:
if config.base_environment == "aio-sandbox":
return AIO_BASE_IMAGE
return _OPERATING_SYSTEMS[config.operating_system][1]


def environment_capabilities(config: EnvironmentInput) -> list[str]:
if config.base_environment == "aio-sandbox":
return ["shell-exec"]
return []


def validate_dockerfile(value: str) -> str:
dockerfile = value.strip()
if "\x00" in dockerfile:
Expand All @@ -315,4 +372,10 @@ def validate_dockerfile(value: str) -> str:
return dockerfile


__all__ = ["build_dockerfile", "validate_dockerfile"]
__all__ = [
"AIO_BASE_IMAGE",
"build_dockerfile",
"environment_base_image",
"environment_capabilities",
"validate_dockerfile",
]
Loading
Loading