feat: introduce attribute management system for sandbox

- Added AttrMap and AttrKey classes for type-safe attribute storage.
- Implemented AppAssetsAttrs and SkillAttrs for managing application and skill attributes.
- Refactored Sandbox and initializers to utilize the new attribute management system, enhancing modularity and clarity in asset handling.
This commit is contained in:
Harry
2026-01-22 15:05:35 +08:00
parent ecd6c44a32
commit e7c3e4cd21
13 changed files with 524 additions and 49 deletions

View File

@ -0,0 +1,7 @@
from core.app.entities.app_asset_entities import AppAssetFileTree
from libs.attr_map import AttrKey
class AppAssetsAttrs:
# Skill artifact set
FILE_TREE = AttrKey("file_tree", AppAssetFileTree)

View File

@ -100,9 +100,6 @@ class SandboxBuilder:
environments=self._environments,
user_id=self._user_id,
)
for init in self._initializers:
init.initialize(vm)
sandbox = Sandbox(
vm=vm,
storage=self._storage,
@ -111,6 +108,9 @@ class SandboxBuilder:
app_id=self._app_id,
assets_id=self._assets_id,
)
for init in self._initializers:
init.initialize(sandbox)
sandbox.mount()
return sandbox

View File

@ -1,10 +1,12 @@
import logging
from core.app_assets.constants import AppAssetsAttrs
from core.app_assets.paths import AssetPaths
from core.sandbox.sandbox import Sandbox
from core.virtual_environment.__base.helpers import pipeline
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from extensions.ext_storage import storage
from extensions.storage.file_presign_storage import FilePresignStorage
from services.app_asset_service import AppAssetService
from ..entities import AppAssets
from .base import SandboxInitializer
@ -20,42 +22,17 @@ class AppAssetsInitializer(SandboxInitializer):
self._app_id = app_id
self._assets_id = assets_id
def initialize(self, env: VirtualEnvironment) -> None:
def initialize(self, sandbox: Sandbox) -> None:
vm = sandbox.vm
# load app assets
app_assets = AppAssetService.get_tenant_app_assets(self._tenant_id, self._assets_id)
sandbox.attrs.set(AppAssetsAttrs.FILE_TREE, app_assets.asset_tree)
zip_key = AssetPaths.build_zip(self._tenant_id, self._app_id, self._assets_id)
download_url = FilePresignStorage(storage.storage_runner).get_download_url(zip_key)
(
pipeline(env)
.add(["wget", "-q", download_url, "-O", AppAssets.ZIP_PATH], error_message="Failed to download assets zip")
# unzip with silent error and return 1 if the zip is empty
# FIXME(Mairuis): should use a more robust way to check if the zip is empty
.add(
["sh", "-c", f"unzip {AppAssets.ZIP_PATH} -d {AppAssets.PATH} 2>/dev/null || [ $? -eq 1 ]"],
error_message="Failed to unzip assets",
)
.execute(timeout=APP_ASSETS_DOWNLOAD_TIMEOUT, raise_on_error=True)
)
logger.info(
"App assets initialized for app_id=%s, published_id=%s",
self._app_id,
self._assets_id,
)
class DraftAppAssetsInitializer(SandboxInitializer):
def __init__(self, tenant_id: str, app_id: str, assets_id: str) -> None:
self._tenant_id = tenant_id
self._app_id = app_id
self._assets_id = assets_id
def initialize(self, env: VirtualEnvironment) -> None:
zip_key = AssetPaths.build_zip(self._tenant_id, self._app_id, self._assets_id)
download_url = FilePresignStorage(storage.storage_runner).get_download_url(zip_key)
(
pipeline(env)
.add(["rm", "-rf", AppAssets.PATH])
pipeline(vm)
.add(["wget", "-q", download_url, "-O", AppAssets.ZIP_PATH], error_message="Failed to download assets zip")
# unzip with silent error and return 1 if the zip is empty
# FIXME(Mairuis): should use a more robust way to check if the zip is empty

View File

@ -1,8 +1,8 @@
from abc import ABC, abstractmethod
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from core.sandbox.sandbox import Sandbox
class SandboxInitializer(ABC):
@abstractmethod
def initialize(self, env: VirtualEnvironment) -> None: ...
def initialize(self, env: Sandbox) -> None: ...

View File

@ -5,10 +5,10 @@ import logging
from io import BytesIO
from pathlib import Path
from core.sandbox.sandbox import Sandbox
from core.session.cli_api import CliApiSessionManager
from core.skill.skill_manager import SkillManager
from core.virtual_environment.__base.helpers import pipeline
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
from ..bash.dify_cli import DifyCliConfig, DifyCliLocator
from ..entities import DifyCli
@ -35,18 +35,19 @@ class DifyCliInitializer(SandboxInitializer):
self._tools = []
self._cli_api_session = None
def initialize(self, env: VirtualEnvironment) -> None:
binary = self._locator.resolve(env.metadata.os, env.metadata.arch)
def initialize(self, sandbox: Sandbox) -> None:
vm = sandbox.vm
binary = self._locator.resolve(vm.metadata.os, vm.metadata.arch)
pipeline(env).add(
pipeline(vm).add(
["mkdir", "-p", f"{DifyCli.ROOT}/bin"], error_message="Failed to create dify CLI directory"
).execute(raise_on_error=True)
env.upload_file(DifyCli.PATH, BytesIO(binary.path.read_bytes()))
vm.upload_file(DifyCli.PATH, BytesIO(binary.path.read_bytes()))
# Use 'cp' with mode preservation workaround: copy file to itself to claim ownership,
# then use 'install' to set executable permission
pipeline(env).add(
pipeline(vm).add(
[
"sh",
"-c",
@ -67,16 +68,16 @@ class DifyCliInitializer(SandboxInitializer):
# FIXME(Mairuis): store it in workflow context
self._cli_api_session = CliApiSessionManager().create(tenant_id=self._tenant_id, user_id=self._user_id)
pipeline(env).add(
pipeline(vm).add(
["mkdir", "-p", DifyCli.GLOBAL_TOOLS_PATH], error_message="Failed to create global tools dir"
).execute(raise_on_error=True)
config = DifyCliConfig.create(self._cli_api_session, self._tenant_id, artifact)
config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False)
config_path = f"{DifyCli.GLOBAL_TOOLS_PATH}/{DifyCli.CONFIG_FILENAME}"
env.upload_file(config_path, BytesIO(config_json.encode("utf-8")))
vm.upload_file(config_path, BytesIO(config_json.encode("utf-8")))
pipeline(env, cwd=DifyCli.GLOBAL_TOOLS_PATH).add(
pipeline(vm, cwd=DifyCli.GLOBAL_TOOLS_PATH).add(
[DifyCli.PATH, "init"], error_message="Failed to initialize Dify CLI"
).execute(raise_on_error=True)

View File

@ -0,0 +1,43 @@
from __future__ import annotations
import logging
from core.sandbox.sandbox import Sandbox
from core.skill import SkillAttrs
from core.skill.skill_manager import SkillManager
from .base import SandboxInitializer
logger = logging.getLogger(__name__)
class SkillInitializer(SandboxInitializer):
def __init__(
self,
tenant_id: str,
user_id: str,
app_id: str,
assets_id: str,
) -> None:
self._tenant_id = tenant_id
self._app_id = app_id
self._user_id = user_id
self._assets_id = assets_id
def initialize(self, sandbox: Sandbox) -> None:
artifact_set = SkillManager.load_artifact(
self._tenant_id,
self._app_id,
self._assets_id,
)
if artifact_set is None:
raise ValueError(
f"No skill artifact set found for tenant_id={self._tenant_id},"
f"app_id={self._app_id}, "
f"assets_id={self._assets_id} "
)
sandbox.attrs.set(
SkillAttrs.ARTIFACT_SET,
artifact_set,
)

View File

@ -7,7 +7,7 @@ from typing import Final
from core.sandbox.builder import SandboxBuilder
from core.sandbox.entities import AppAssets, SandboxType
from core.sandbox.entities.providers import SandboxProviderEntity
from core.sandbox.initializer.app_assets_initializer import AppAssetsInitializer, DraftAppAssetsInitializer
from core.sandbox.initializer.app_assets_initializer import AppAssetsInitializer
from core.sandbox.initializer.dify_cli_initializer import DifyCliInitializer
from core.sandbox.sandbox import Sandbox
from core.sandbox.storage.archive_storage import ArchiveSandboxStorage
@ -151,7 +151,7 @@ class SandboxManager:
.options(sandbox_provider.config)
.user(user_id)
.app(app_id)
.initializer(DraftAppAssetsInitializer(tenant_id, app_id, assets.id))
.initializer(AppAssetsInitializer(tenant_id, app_id, assets.id))
.initializer(DifyCliInitializer(tenant_id, user_id, app_id, assets.id))
.storage(storage, assets.id)
.build()

View File

@ -3,6 +3,8 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from libs.attr_map import AttrMap
if TYPE_CHECKING:
from core.sandbox.storage.sandbox_storage import SandboxStorage
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
@ -27,6 +29,11 @@ class Sandbox:
self._user_id = user_id
self._app_id = app_id
self._assets_id = assets_id
self._attributes = AttrMap()
@property
def attrs(self) -> AttrMap:
return self._attributes
@property
def vm(self) -> VirtualEnvironment:

View File

@ -1,7 +1,9 @@
from .constants import SkillAttrs
from .entities import ToolArtifact, ToolDependency, ToolReference
from .skill_manager import SkillManager
__all__ = [
"SkillAttrs",
"SkillManager",
"ToolArtifact",
"ToolDependency",

View File

@ -0,0 +1,7 @@
from core.skill.entities.skill_artifact_set import SkillArtifactSet
from libs.attr_map import AttrKey
class SkillAttrs:
# Skill artifact set
ARTIFACT_SET = AttrKey("skill_artifact_set", SkillArtifactSet)