From bfeb8315aafca559eb7c569b1ac0886ae595f4c1 Mon Sep 17 00:00:00 2001 From: youhuanghe <1051233107@qq.com> Date: Tue, 24 Mar 2026 01:45:01 +0000 Subject: [PATCH] feat: enhance sandbox api --- src/langbot/pkg/box/service.py | 1 + .../pkg/provider/tools/loaders/native.py | 13 ++++++++- tests/unit_tests/box/test_box_service.py | 28 +++++++++++++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/langbot/pkg/box/service.py b/src/langbot/pkg/box/service.py index 9b3e85f2..ed52987e 100644 --- a/src/langbot/pkg/box/service.py +++ b/src/langbot/pkg/box/service.py @@ -208,6 +208,7 @@ class BoxService: return { 'session_id': spec.session_id, 'workdir': spec.workdir, + 'mount_path': spec.mount_path, 'timeout_sec': spec.timeout_sec, 'network': spec.network.value, 'image': spec.image, diff --git a/src/langbot/pkg/provider/tools/loaders/native.py b/src/langbot/pkg/provider/tools/loaders/native.py index 4e13a780..f9c94a6f 100644 --- a/src/langbot/pkg/provider/tools/loaders/native.py +++ b/src/langbot/pkg/provider/tools/loaders/native.py @@ -55,7 +55,18 @@ class NativeToolLoader(loader.ToolLoader): }, 'workdir': { 'type': 'string', - 'description': 'Absolute working directory path inside the sandbox. Defaults to /workspace.', + 'description': ( + 'Absolute working directory path inside the sandbox. ' + 'Defaults to mount_path, or /workspace when mount_path is omitted.' + ), + 'default': '/workspace', + }, + 'mount_path': { + 'type': 'string', + 'description': ( + 'Absolute sandbox path where host_path is mounted. ' + 'Defaults to /workspace. When omitted, workdir defaults to the same path.' + ), 'default': '/workspace', }, 'timeout_sec': { diff --git a/tests/unit_tests/box/test_box_service.py b/tests/unit_tests/box/test_box_service.py index 523ff3e9..f2bca413 100644 --- a/tests/unit_tests/box/test_box_service.py +++ b/tests/unit_tests/box/test_box_service.py @@ -99,6 +99,7 @@ class FakeBackend(BaseSandboxBackend): network=spec.network, host_path=spec.host_path, host_path_mode=spec.host_path_mode, + mount_path=spec.mount_path, cpus=spec.cpus, memory_mb=spec.memory_mb, pids_limit=spec.pids_limit, @@ -1017,7 +1018,7 @@ class TestBoxHostMountModeNone: assert spec.workdir == '/opt/custom' def test_spec_with_rw_mode_requires_workspace_workdir(self): - """When host_path_mode is RW, workdir must be under /workspace.""" + """When host_path_mode is RW, workdir must be under mount_path.""" with pytest.raises(Exception): BoxSpec( session_id='test', @@ -1028,7 +1029,7 @@ class TestBoxHostMountModeNone: ) def test_spec_with_ro_mode_requires_workspace_workdir(self): - """When host_path_mode is RO, workdir must be under /workspace.""" + """When host_path_mode is RO, workdir must be under mount_path.""" with pytest.raises(Exception): BoxSpec( session_id='test', @@ -1037,3 +1038,26 @@ class TestBoxHostMountModeNone: host_path_mode=BoxHostMountMode.READ_ONLY, workdir='/opt/custom', ) + + def test_spec_with_custom_mount_path_allows_matching_workdir(self): + spec = BoxSpec( + session_id='test', + cmd='echo hi', + host_path='/home/user/data', + host_path_mode=BoxHostMountMode.READ_WRITE, + mount_path='/project', + workdir='/project/src', + ) + assert spec.mount_path == '/project' + assert spec.workdir == '/project/src' + + def test_spec_with_custom_mount_path_rejects_outside_workdir(self): + with pytest.raises(Exception): + BoxSpec( + session_id='test', + cmd='echo hi', + host_path='/home/user/data', + host_path_mode=BoxHostMountMode.READ_WRITE, + mount_path='/project', + workdir='/workspace', + )