Compare commits

...

1 Commits

Author SHA1 Message Date
openhands
8f14520a87 Add support for .openhands/setup.sh script
- Add _run_setup_script method to Session class to run setup.sh after runtime initialization
- Add get_full_path method to FileStore interface and implementations
- Run setup script with proper permissions and error handling
2025-01-02 20:59:42 +00:00
7 changed files with 62 additions and 0 deletions

View File

@@ -5,6 +5,11 @@ class E2BFileStore(FileStore):
def __init__(self, filesystem):
self.filesystem = filesystem
def get_full_path(self, path: str) -> str:
if path.startswith('/'):
path = path[1:]
return path
def write(self, path: str, contents: str) -> None:
self.filesystem.write(path, contents)

View File

@@ -1,4 +1,6 @@
import asyncio
import os
import subprocess
import time
from copy import deepcopy
@@ -112,6 +114,9 @@ class Session:
github_token=github_token,
selected_repository=selected_repository,
)
# Run setup script if it exists
await self._run_setup_script()
except Exception as e:
logger.exception(f'Error creating controller: {e}')
await self.send_error(
@@ -193,6 +198,29 @@ class Session:
"""Sends an error message to the client."""
await self.send({'error': True, 'message': message})
async def _run_setup_script(self):
"""Run the .openhands/setup.sh script if it exists."""
setup_script = self.file_store.get_full_path('.openhands/setup.sh')
if os.path.isfile(setup_script):
try:
# Make sure the script is executable
os.chmod(setup_script, 0o755)
process = await asyncio.create_subprocess_exec(
setup_script,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=os.path.dirname(setup_script),
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode() if stderr else stdout.decode()
logger.error(f'Setup script failed: {error_msg}')
await self.send_error(f'Setup script failed: {error_msg}')
except Exception as e:
logger.error(f'Failed to run setup script: {e}')
await self.send_error(f'Failed to run setup script: {e}')
async def _send_status_message(self, msg_type: str, id: str, message: str):
"""Sends a status message to the client."""
if msg_type == 'error':

View File

@@ -2,6 +2,18 @@ from abc import abstractmethod
class FileStore:
@abstractmethod
def get_full_path(self, path: str) -> str:
"""Get the full path for a given relative path.
Args:
path: The relative path.
Returns:
The full path.
"""
pass
@abstractmethod
def write(self, path: str, contents: str) -> None:
pass

View File

@@ -19,6 +19,11 @@ class GoogleCloudFileStore(FileStore):
self.storage_client = storage.Client()
self.bucket = self.storage_client.bucket(bucket_name)
def get_full_path(self, path: str) -> str:
if path.startswith('/'):
path = path[1:]
return path
def write(self, path: str, contents: str | bytes) -> None:
blob = self.bucket.blob(path)
with blob.open('w') as f:

View File

@@ -12,6 +12,11 @@ class InMemoryFileStore(FileStore):
def __init__(self, files: dict[str, str] = IN_MEMORY_FILES):
self.files = files
def get_full_path(self, path: str) -> str:
if path.startswith('/'):
path = path[1:]
return path
def write(self, path: str, contents: str) -> None:
self.files[path] = contents

View File

@@ -15,6 +15,11 @@ class S3FileStore(FileStore):
self.bucket = os.getenv('AWS_S3_BUCKET')
self.client = Minio(endpoint, access_key, secret_key, secure=secure)
def get_full_path(self, path: str) -> str:
if path.startswith('/'):
path = path[1:]
return path
def write(self, path: str, contents: str) -> None:
as_bytes = contents.encode('utf-8')
stream = io.BytesIO(as_bytes)

View File

@@ -100,6 +100,7 @@ reportlab = "*"
[tool.coverage.run]
concurrency = ["gevent"]
[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
@@ -129,6 +130,7 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"