Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions openhands/automation/preset_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io
import json
import logging
import os
import tarfile
import uuid
from collections.abc import AsyncIterator
Expand Down Expand Up @@ -48,11 +49,18 @@
PROMPT_PRESET_DIR = PRESETS_DIR / "prompt"
PLUGIN_PRESET_DIR = PRESETS_DIR / "plugin"

# Venv Python entrypoint (Unix path format)
# - Cloud mode: Always Linux sandboxes, so Unix paths work
# - Local mode: Requires Unix-like environment (Linux, macOS, WSL)
# - Native Windows is not currently supported for local mode
VENV_ENTRYPOINT = ".venv/bin/python main.py"
def _get_preset_entrypoint() -> str:
"""Return the preset entrypoint for the current host platform.

Preset automations create their virtual environment inside the run working
directory. Cloud sandboxes use the POSIX layout (``.venv/bin/python``),
while native Windows uses ``.venv/Scripts/python.exe``.
"""
python_path = (
".venv/Scripts/python.exe" if os.name == "nt" else ".venv/bin/python"
)
return f"{python_path} main.py"


# Preset file caches to avoid I/O on every request
_PROMPT_PRESET_CACHE: dict[str, str] | None = None
Expand Down Expand Up @@ -434,7 +442,7 @@ async def create_automation_from_prompt(
trigger=body.trigger.model_dump(),
tarball_path=tarball_path,
setup_script_path="setup.sh",
entrypoint=VENV_ENTRYPOINT,
entrypoint=_get_preset_entrypoint(),
timeout=body.timeout,
)
session.add(automation)
Expand Down Expand Up @@ -751,7 +759,7 @@ async def create_automation_from_plugin(
trigger=body.trigger.model_dump(),
tarball_path=tarball_path,
setup_script_path="setup.sh",
entrypoint=VENV_ENTRYPOINT,
entrypoint=_get_preset_entrypoint(),
timeout=body.timeout,
)
session.add(automation)
Expand Down
13 changes: 12 additions & 1 deletion openhands/automation/presets/plugin/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@
set -e

echo "[setup] Fetching SDK version from automation service"
PYTHON_JSON=python3
if ! command -v python3 >/dev/null 2>&1; then
if command -v python >/dev/null 2>&1; then
PYTHON_JSON=python
elif command -v py >/dev/null 2>&1; then
PYTHON_JSON='py -3'
else
echo "[setup] ERROR: python3, python, or py is required to parse SDK version" >&2
exit 1
fi
fi
set +e
SDK_VERSION=$(curl -sf "${AUTOMATION_API_URL}/sdk-version" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['version'])" 2>/dev/null)
| ${PYTHON_JSON} -c "import sys, json; print(json.load(sys.stdin)['version'])" 2>/dev/null)
set -e
if [ -z "$SDK_VERSION" ]; then
echo "[setup] ERROR: Failed to fetch SDK version from ${AUTOMATION_API_URL}/sdk-version" >&2
Expand Down
13 changes: 12 additions & 1 deletion openhands/automation/presets/prompt/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@
set -e

echo "[setup] Fetching SDK version from automation service"
PYTHON_JSON=python3
if ! command -v python3 >/dev/null 2>&1; then
if command -v python >/dev/null 2>&1; then
PYTHON_JSON=python
elif command -v py >/dev/null 2>&1; then
PYTHON_JSON='py -3'
else
echo "[setup] ERROR: python3, python, or py is required to parse SDK version" >&2
exit 1
fi
fi
set +e
SDK_VERSION=$(curl -sf "${AUTOMATION_API_URL}/sdk-version" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['version'])" 2>/dev/null)
| ${PYTHON_JSON} -c "import sys, json; print(json.load(sys.stdin)['version'])" 2>/dev/null)
set -e
if [ -z "$SDK_VERSION" ]; then
echo "[setup] ERROR: Failed to fetch SDK version from ${AUTOMATION_API_URL}/sdk-version" >&2
Expand Down
33 changes: 29 additions & 4 deletions tests/test_preset_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from openhands.automation.preset_router import (
_generate_plugin_tarball,
_generate_tarball,
_get_preset_entrypoint,
_replace_prompt_in_tarball,
)
from openhands.sdk.plugin import PluginSource
Expand Down Expand Up @@ -112,6 +113,30 @@ def test_plugin_setup_sh_fetches_sdk_version_from_api(self):
)


class TestPresetEntrypoint:
def test_get_preset_entrypoint_posix(self, monkeypatch):
monkeypatch.setattr("openhands.automation.preset_router.os.name", "posix")
assert _get_preset_entrypoint() == ".venv/bin/python main.py"

def test_get_preset_entrypoint_windows(self, monkeypatch):
monkeypatch.setattr("openhands.automation.preset_router.os.name", "nt")
assert _get_preset_entrypoint() == ".venv/Scripts/python.exe main.py"

def test_prompt_setup_sh_falls_back_when_python3_missing(self):
setup_sh_path = PRESETS_DIR / "prompt" / "setup.sh"
content = setup_sh_path.read_text()
assert "command -v python3" in content
assert "command -v python" in content
assert "command -v py" in content

def test_plugin_setup_sh_falls_back_when_python3_missing(self):
setup_sh_path = PRESETS_DIR / "plugin" / "setup.sh"
content = setup_sh_path.read_text()
assert "command -v python3" in content
assert "command -v python" in content
assert "command -v py" in content


class TestGenerateTarball:
"""Tests for the tarball generation function."""

Expand Down Expand Up @@ -427,7 +452,7 @@ async def test_create_from_prompt_success(
assert data["prompt"] == test_prompt
assert data["trigger"]["type"] == "cron"
assert data["trigger"]["schedule"] == "0 9 * * 1"
assert data["entrypoint"] == ".venv/bin/python main.py"
assert data["entrypoint"] == _get_preset_entrypoint()
assert data["setup_script_path"] == "setup.sh"
assert data["tarball_path"].startswith("oh-internal://uploads/")
assert data["enabled"] is True
Expand Down Expand Up @@ -554,7 +579,7 @@ async def test_create_from_prompt_creates_automation_record(
assert automation is not None
assert automation.name == "Automation Record Test"
assert automation.prompt == "Print hello"
assert automation.entrypoint == ".venv/bin/python main.py"
assert automation.entrypoint == _get_preset_entrypoint()
assert automation.setup_script_path == "setup.sh"
assert automation.timeout == 300
assert automation.user_id == TEST_USER_ID
Expand Down Expand Up @@ -1283,7 +1308,7 @@ async def test_create_from_plugin_success(
assert data["prompt"] == "Review all Python files for security issues"
assert data["trigger"]["type"] == "cron"
assert data["trigger"]["schedule"] == "0 9 * * 1"
assert data["entrypoint"] == ".venv/bin/python main.py"
assert data["entrypoint"] == _get_preset_entrypoint()
assert data["setup_script_path"] == "setup.sh"
assert data["tarball_path"].startswith("oh-internal://uploads/")
assert data["enabled"] is True
Expand Down Expand Up @@ -1399,7 +1424,7 @@ async def test_create_from_plugin_creates_automation_record(
assert automation is not None
assert automation.name == "Automation Record Test"
assert automation.prompt == "Run plugin tasks"
assert automation.entrypoint == ".venv/bin/python main.py"
assert automation.entrypoint == _get_preset_entrypoint()
assert automation.setup_script_path == "setup.sh"
assert automation.timeout == 300
assert automation.user_id == TEST_USER_ID
Expand Down
Loading