diff --git a/src/langbot/pkg/box/workspace.py b/src/langbot/pkg/box/workspace.py index 41fca039..26d1a41e 100644 --- a/src/langbot/pkg/box/workspace.py +++ b/src/langbot/pkg/box/workspace.py @@ -204,14 +204,14 @@ def wrap_python_command_with_env(command: str, *, mount_path: str = '/workspace' fi if [ "$_LB_NEEDS_BOOTSTRAP" -eq 1 ]; then - if [ -d "$_LB_LOCK_DIR" ] && [ ! -f "$_LB_LOCK_DIR/pid" ]; then - echo "Clearing stale Python environment lock without owner: $_LB_LOCK_DIR" >&2 - rm -rf "$_LB_LOCK_DIR" 2>/dev/null || true - fi - _LB_LOCK_WAIT=0 while ! mkdir "$_LB_LOCK_DIR" 2>/dev/null; do if [ "$_LB_LOCK_WAIT" -ge 120 ]; then + _LB_LOCK_OWNER="$(cat "$_LB_LOCK_DIR/pid" 2>/dev/null || true)" + if [ -n "$_LB_LOCK_OWNER" ] && kill -0 "$_LB_LOCK_OWNER" 2>/dev/null; then + echo "Timed out waiting for active Python environment lock: $_LB_LOCK_DIR" >&2 + exit 1 + fi echo "Timed out waiting for Python environment lock, clearing stale lock: $_LB_LOCK_DIR" >&2 rm -rf "$_LB_LOCK_DIR" 2>/dev/null || true if mkdir "$_LB_LOCK_DIR" 2>/dev/null; then diff --git a/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py b/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py index 736dacea..b74b077b 100644 --- a/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py +++ b/src/langbot/pkg/provider/tools/loaders/mcp_stdio.py @@ -276,7 +276,7 @@ class BoxStdioSessionRuntime: # to delete them, so refresh source files in place and preserve runtime # directories instead of rmtree'ing the whole staging root. with _workspace_copy_lock(process_host_root): - preserved_names = {'.venv', 'venv', 'env', '.env', '.cache', '.tmp', '.langbot'} + preserved_names = {'.venv', 'venv', 'env', '.cache', '.tmp', '.langbot'} os.makedirs(process_host_workspace, exist_ok=True) for name in os.listdir(process_host_workspace): if name in preserved_names: @@ -288,6 +288,7 @@ class BoxStdioSessionRuntime: try: os.unlink(path) except FileNotFoundError: + # The entry may disappear between listdir and unlink if cleanup races us. pass shutil.copytree( source_path, @@ -303,7 +304,6 @@ class BoxStdioSessionRuntime: '.venv', 'venv', 'env', - '.env', '.cache', '.tmp', '.langbot', @@ -401,18 +401,12 @@ class BoxStdioSessionRuntime: workspace_root = workspace_path.rstrip('/') or '/workspace' venv_dir = f'{workspace_root}/.venv' venv_bin = f'{venv_dir}/bin' - command = ' '.join( - [shlex.quote(payload['command']), *[shlex.quote(arg) for arg in payload.get('args', [])]] - ) + command = ' '.join([shlex.quote(payload['command']), *[shlex.quote(arg) for arg in payload.get('args', [])]]) wrapped = dict(payload) wrapped['command'] = 'sh' wrapped['args'] = [ '-lc', - ( - f'export VIRTUAL_ENV={shlex.quote(venv_dir)}; ' - f'export PATH={shlex.quote(venv_bin)}:$PATH; ' - f'exec {command}' - ), + (f'export VIRTUAL_ENV={shlex.quote(venv_dir)}; export PATH={shlex.quote(venv_bin)}:$PATH; exec {command}'), ] return wrapped diff --git a/tests/unit_tests/box/test_workspace.py b/tests/unit_tests/box/test_workspace.py index e62d8522..e4620ad3 100644 --- a/tests/unit_tests/box/test_workspace.py +++ b/tests/unit_tests/box/test_workspace.py @@ -56,10 +56,7 @@ def test_wrap_python_command_with_env_contains_bootstrap_and_command(): assert '_LB_SYSTEM_PYTHON="$(command -v python3 || command -v python || true)"' in command assert '"$_LB_SYSTEM_PYTHON" -m venv "$_LB_VENV_DIR"' in command - assert 'Clearing stale Python environment lock without owner: $_LB_LOCK_DIR' in command - assert 'clearing stale lock: $_LB_LOCK_DIR' in command - assert 'printf \'%s\\n\' "$$" > "$_LB_LOCK_DIR/pid"' in command - assert 'rm -rf "$_LB_LOCK_DIR"' in command + assert 'kill -0 "$_LB_LOCK_OWNER"' in command assert 'export VIRTUAL_ENV="$_LB_VENV_DIR"' in command assert command.rstrip().endswith('python script.py') diff --git a/tests/unit_tests/provider/test_mcp_box_integration.py b/tests/unit_tests/provider/test_mcp_box_integration.py index c7e080c0..598f7dce 100644 --- a/tests/unit_tests/provider/test_mcp_box_integration.py +++ b/tests/unit_tests/provider/test_mcp_box_integration.py @@ -519,6 +519,7 @@ class TestPythonWorkspacePreparation: source.mkdir() (source / 'server.py').write_text('print("new")\n', encoding='utf-8') (source / 'requirements.txt').write_text('mcp==1.26.0\n', encoding='utf-8') + (source / '.env').write_text('TOKEN=new\n', encoding='utf-8') process_root = tmp_path / 'shared' / '.mcp' / 'u1' workspace = process_root / 'workspace' @@ -526,6 +527,7 @@ class TestPythonWorkspacePreparation: (workspace / '.venv' / 'bin' / 'python').write_text('', encoding='utf-8') (workspace / '.langbot').mkdir() (workspace / '.langbot' / 'python-env.lock').mkdir() + (workspace / '.env').write_text('TOKEN=old\n', encoding='utf-8') (workspace / 'server.py').write_text('print("old")\n', encoding='utf-8') (workspace / 'removed.py').write_text('stale\n', encoding='utf-8') (workspace / 'removed_dir').mkdir() @@ -535,6 +537,7 @@ class TestPythonWorkspacePreparation: assert (workspace / 'server.py').read_text(encoding='utf-8') == 'print("new")\n' assert (workspace / 'requirements.txt').read_text(encoding='utf-8') == 'mcp==1.26.0\n' + assert (workspace / '.env').read_text(encoding='utf-8') == 'TOKEN=new\n' assert not (workspace / 'removed.py').exists() assert not (workspace / 'removed_dir').exists() assert (workspace / '.venv' / 'bin' / 'python').exists()