mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 17:36:07 +00:00
refactor(box): derive paths from shared host root
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user