refactor(box): derive paths from shared host root

This commit is contained in:
youhuanghe
2026-03-24 02:23:35 +00:00
committed by WangCham
parent bfeb8315aa
commit 63d22b1f8e
4 changed files with 60 additions and 11 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ services:
# Uncomment the one that matches your container runtime: # Uncomment the one that matches your container runtime:
# - /var/run/podman/podman.sock:/var/run/podman/podman.sock # Podman # - /var/run/podman/podman.sock:/var/run/podman/podman.sock # Podman
- /var/run/docker.sock:/var/run/docker.sock # Docker - /var/run/docker.sock:/var/run/docker.sock # Docker
- ./data/box-workspaces:/workspaces - ./data/box:/workspaces
ports: ports:
- 5410:5410 - 5410:5410
restart: on-failure restart: on-failure
@@ -41,7 +41,7 @@ services:
container_name: langbot container_name: langbot
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./data/box-workspaces:/workspaces - ./data/box:/workspaces
restart: on-failure restart: on-failure
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
+31 -5
View File
@@ -50,6 +50,7 @@ class BoxService:
client = self._runtime_connector.client client = self._runtime_connector.client
self.client = client self.client = client
self.output_limit_chars = output_limit_chars self.output_limit_chars = output_limit_chars
self.shared_host_root = self._load_shared_host_root()
self.allowed_host_mount_roots = self._load_allowed_host_mount_roots() self.allowed_host_mount_roots = self._load_allowed_host_mount_roots()
self.default_host_workspace = self._load_default_host_workspace() self.default_host_workspace = self._load_default_host_workspace()
self.profile = self._load_profile() self.profile = self._load_profile()
@@ -73,13 +74,17 @@ class BoxService:
def available(self) -> bool: def available(self) -> bool:
return self._available return self._available
async def execute_sandbox_tool(self, parameters: dict, query: 'pipeline_query.Query') -> dict: async def execute_spec_payload(
self,
spec_payload: dict,
query: 'pipeline_query.Query',
*,
skip_host_mount_validation: bool = False,
) -> dict:
if not self._available: if not self._available:
raise BoxError('Box runtime is not available. Install and start Podman or Docker to use sandbox features.') raise BoxError('Box runtime is not available. Install and start Podman or Docker to use sandbox features.')
spec_payload = dict(parameters)
spec_payload.setdefault('session_id', str(query.query_id))
try: try:
spec = self.build_spec(spec_payload) spec = self.build_spec(spec_payload, skip_host_mount_validation=skip_host_mount_validation)
except BoxError as exc: except BoxError as exc:
self._record_error(exc, query) self._record_error(exc, query)
raise raise
@@ -100,6 +105,11 @@ class BoxService:
) )
return self._serialize_result(result) return self._serialize_result(result)
async def execute_sandbox_tool(self, parameters: dict, query: 'pipeline_query.Query') -> dict:
spec_payload = dict(parameters)
spec_payload.setdefault('session_id', str(query.query_id))
return await self.execute_spec_payload(spec_payload, query)
async def shutdown(self): async def shutdown(self):
await self.client.shutdown() await self.client.shutdown()
@@ -250,14 +260,30 @@ class BoxService:
continue continue
normalized_roots.append(os.path.realpath(os.path.abspath(root_value))) normalized_roots.append(os.path.realpath(os.path.abspath(root_value)))
if not normalized_roots and self.shared_host_root is not None:
normalized_roots.append(self.shared_host_root)
return normalized_roots return normalized_roots
def _load_shared_host_root(self) -> str | None:
shared_host_root = str(_get_box_config(self.ap).get('shared_host_root', '')).strip()
if not shared_host_root:
return None
return os.path.realpath(os.path.abspath(shared_host_root))
def _load_default_host_workspace(self) -> str | None: def _load_default_host_workspace(self) -> str | None:
default_host_workspace = str(_get_box_config(self.ap).get('default_host_workspace', '')).strip() default_host_workspace = str(_get_box_config(self.ap).get('default_host_workspace', '')).strip()
if not default_host_workspace: if not default_host_workspace:
return None if self.shared_host_root is None:
return None
default_host_workspace = os.path.join(self.shared_host_root, 'default')
return os.path.realpath(os.path.abspath(default_host_workspace)) return os.path.realpath(os.path.abspath(default_host_workspace))
def get_managed_skills_root(self) -> str | None:
if self.shared_host_root is None:
return None
return os.path.join(self.shared_host_root, 'skills')
def _ensure_default_host_workspace(self): def _ensure_default_host_workspace(self):
if self.default_host_workspace is None: if self.default_host_workspace is None:
return return
+3 -3
View File
@@ -90,9 +90,9 @@ monitoring:
box: box:
profile: 'default' profile: 'default'
runtime_url: '' # Leave empty to use defaults: http://127.0.0.1:5410 locally, http://langbot_box_runtime:5410 in Docker runtime_url: '' # Leave empty to use defaults: http://127.0.0.1:5410 locally, http://langbot_box_runtime:5410 in Docker
default_host_workspace: './data/box-workspaces/default' # For Docker deployment, use '/workspaces/default' shared_host_root: './data/box' # For Docker deployment, use '/workspaces'
allowed_host_mount_roots: # For Docker deployment, use '/workspaces' instead default_host_workspace: '' # Defaults to '<shared_host_root>/default'
- './data/box-workspaces' allowed_host_mount_roots: # Defaults to ['<shared_host_root>'] when left empty
- '/tmp' - '/tmp'
space: space:
# Space service URL for OAuth and API # Space service URL for OAuth and API
+24 -1
View File
@@ -128,13 +128,19 @@ def make_query(query_id: int = 42) -> pipeline_query.Query:
return pipeline_query.Query.model_construct(query_id=query_id) return pipeline_query.Query.model_construct(query_id=query_id)
def make_app(logger: Mock, allowed_host_mount_roots: list[str] | None = None, profile: str = 'default'): def make_app(
logger: Mock,
allowed_host_mount_roots: list[str] | None = None,
profile: str = 'default',
shared_host_root: str = '',
):
return SimpleNamespace( return SimpleNamespace(
logger=logger, logger=logger,
instance_config=SimpleNamespace( instance_config=SimpleNamespace(
data={ data={
'box': { 'box': {
'profile': profile, 'profile': profile,
'shared_host_root': shared_host_root,
'allowed_host_mount_roots': allowed_host_mount_roots or [], 'allowed_host_mount_roots': allowed_host_mount_roots or [],
'default_host_workspace': '', 'default_host_workspace': '',
} }
@@ -309,6 +315,23 @@ async def test_box_service_creates_default_host_workspace_on_initialize(tmp_path
assert default_host_workspace.is_dir() assert default_host_workspace.is_dir()
@pytest.mark.asyncio
async def test_box_service_derives_workspace_and_allowed_root_from_shared_host_root(tmp_path):
logger = Mock()
backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
shared_root = tmp_path / 'shared-box-root'
app = make_app(logger, shared_host_root=str(shared_root))
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize()
assert service.shared_host_root == os.path.realpath(shared_root)
assert service.default_host_workspace == os.path.realpath(shared_root / 'default')
assert service.allowed_host_mount_roots == [os.path.realpath(shared_root)]
assert (shared_root / 'default').is_dir()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_box_service_rejects_host_mount_outside_allowed_roots(tmp_path): async def test_box_service_rejects_host_mount_outside_allowed_roots(tmp_path):
logger = Mock() logger = Mock()