mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
5 Commits
jps/teleme
...
add-device
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a3464a28 | ||
|
|
53871f206b | ||
|
|
4f0bf5d474 | ||
|
|
fc758f2bdf | ||
|
|
d90579b398 |
@@ -4,9 +4,12 @@ import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from storage.device_code import DeviceCode
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class DeviceCodeStore:
|
||||
"""Store for managing OAuth 2.0 device codes."""
|
||||
@@ -165,3 +168,38 @@ class DeviceCodeStore:
|
||||
session.commit()
|
||||
|
||||
return True
|
||||
|
||||
def cleanup_stale_device_codes(self, limit: int = 100) -> int:
|
||||
"""Clean up expired device codes based on oldest creation dates.
|
||||
|
||||
Removes device codes that are expired (past their expires_at time).
|
||||
|
||||
Args:
|
||||
limit: Maximum number of codes to delete
|
||||
|
||||
Returns:
|
||||
Total number of device codes deleted
|
||||
"""
|
||||
with self.session_maker() as session:
|
||||
# Get expired device codes, ordered by oldest first (using ID as proxy for creation order)
|
||||
expired_codes = (
|
||||
session.query(DeviceCode)
|
||||
.filter(DeviceCode.expires_at < datetime.now(timezone.utc))
|
||||
.order_by(DeviceCode.id.asc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not expired_codes:
|
||||
logger.info('No expired device codes found')
|
||||
return 0
|
||||
|
||||
# Delete the expired codes
|
||||
code_ids = [code.id for code in expired_codes]
|
||||
delete_stmt = delete(DeviceCode).where(DeviceCode.id.in_(code_ids))
|
||||
result = session.execute(delete_stmt)
|
||||
session.commit()
|
||||
|
||||
deleted_count = result.rowcount
|
||||
logger.info(f'Deleted {deleted_count} expired device codes')
|
||||
return deleted_count
|
||||
|
||||
30
enterprise/sync/clean_device_code_table.py
Normal file
30
enterprise/sync/clean_device_code_table.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Cleanup script for device code table.
|
||||
|
||||
This script removes expired device codes from the database. Device codes are considered
|
||||
expired if they are past their expires_at time.
|
||||
|
||||
The cleanup is limited to a maximum number of codes to avoid overwhelming the database.
|
||||
|
||||
Usage:
|
||||
python sync/clean_device_code_table.py
|
||||
|
||||
This script should be run periodically (e.g., via cron job) to maintain database hygiene.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from storage.database import session_maker
|
||||
from storage.device_code_store import DeviceCodeStore
|
||||
|
||||
LIMIT = 100 # Maximum number of device codes to delete
|
||||
|
||||
|
||||
async def main():
|
||||
device_code_store = DeviceCodeStore(session_maker)
|
||||
deleted_count = device_code_store.cleanup_stale_device_codes(limit=LIMIT)
|
||||
print(f'Cleanup completed. Deleted {deleted_count} expired device codes.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -191,3 +191,65 @@ class TestDeviceCodeStore:
|
||||
assert result is True
|
||||
mock_device.deny.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
def test_cleanup_stale_device_codes_empty(self, device_code_store, mock_session):
|
||||
"""Test cleanup when no expired device codes exist."""
|
||||
# Mock empty query result
|
||||
mock_session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = []
|
||||
|
||||
result = device_code_store.cleanup_stale_device_codes(limit=50)
|
||||
|
||||
assert result == 0
|
||||
mock_session.query.assert_called_once_with(DeviceCode)
|
||||
|
||||
def test_cleanup_stale_device_codes_with_data(
|
||||
self, device_code_store, mock_session
|
||||
):
|
||||
"""Test cleanup when expired device codes exist."""
|
||||
# Create mock device codes
|
||||
mock_device1 = MagicMock()
|
||||
mock_device1.id = 1
|
||||
mock_device2 = MagicMock()
|
||||
mock_device2.id = 2
|
||||
|
||||
# Mock query result with 2 expired codes
|
||||
mock_session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
|
||||
mock_device1,
|
||||
mock_device2,
|
||||
]
|
||||
|
||||
# Mock the delete execution result
|
||||
mock_result = MagicMock()
|
||||
mock_result.rowcount = 2
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
result = device_code_store.cleanup_stale_device_codes(limit=50)
|
||||
|
||||
assert result == 2
|
||||
mock_session.execute.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
def test_cleanup_stale_device_codes_with_limit(
|
||||
self, device_code_store, mock_session
|
||||
):
|
||||
"""Test cleanup respects the limit parameter."""
|
||||
# Create mock device codes
|
||||
mock_devices = [MagicMock(id=i) for i in range(1, 4)] # 3 codes
|
||||
|
||||
# Mock query result with 3 expired codes
|
||||
mock_session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = mock_devices
|
||||
|
||||
# Mock the delete execution result
|
||||
mock_result = MagicMock()
|
||||
mock_result.rowcount = 3
|
||||
mock_session.execute.return_value = mock_result
|
||||
|
||||
result = device_code_store.cleanup_stale_device_codes(limit=3)
|
||||
|
||||
assert result == 3
|
||||
mock_session.execute.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
# Verify the limit was applied in the query
|
||||
mock_session.query.return_value.filter.return_value.order_by.return_value.limit.assert_called_with(
|
||||
3
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import atexit
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import browsergym.core # noqa F401 (we register the openended task as a gym environment)
|
||||
import gymnasium as gym
|
||||
@@ -67,6 +69,16 @@ class BrowserEnv:
|
||||
raise BrowserInitException('Failed to start browser environment.')
|
||||
|
||||
def browser_process(self) -> None:
|
||||
def _is_local_runtime() -> bool:
|
||||
runtime_flag = os.getenv('RUNTIME', '').lower()
|
||||
return runtime_flag == 'local'
|
||||
|
||||
# Default Playwright cache for local runs only; do not override in docker
|
||||
if _is_local_runtime() and 'PLAYWRIGHT_BROWSERS_PATH' not in os.environ:
|
||||
os.environ['PLAYWRIGHT_BROWSERS_PATH'] = str(
|
||||
Path.home() / '.cache' / 'playwright'
|
||||
)
|
||||
|
||||
if self.eval_mode:
|
||||
assert self.browsergym_eval_env is not None
|
||||
logger.info('Initializing browser env for web browsing evaluation.')
|
||||
@@ -87,6 +99,11 @@ class BrowserEnv:
|
||||
)
|
||||
env = gym.make(self.browsergym_eval_env, tags_to_mark='all', timeout=100000)
|
||||
else:
|
||||
downloads_path = os.getenv('BROWSERGYM_DOWNLOAD_DIR')
|
||||
if not downloads_path and _is_local_runtime():
|
||||
downloads_path = str(Path.home() / '.cache' / 'browsergym-downloads')
|
||||
if not downloads_path:
|
||||
downloads_path = '/workspace/.downloads/'
|
||||
env = gym.make(
|
||||
'browsergym/openended',
|
||||
task_kwargs={'start_url': 'about:blank', 'goal': 'PLACEHOLDER_GOAL'},
|
||||
@@ -96,7 +113,7 @@ class BrowserEnv:
|
||||
tags_to_mark='all',
|
||||
timeout=100000,
|
||||
pw_context_kwargs={'accept_downloads': True},
|
||||
pw_chromium_kwargs={'downloads_path': '/workspace/.downloads/'},
|
||||
pw_chromium_kwargs={'downloads_path': downloads_path},
|
||||
)
|
||||
obs, info = env.reset()
|
||||
|
||||
|
||||
@@ -249,7 +249,22 @@ class LocalRuntime(ActionExecutionClient):
|
||||
)
|
||||
else:
|
||||
# Set up workspace directory
|
||||
# For local runtime, prefer a stable host path over /workspace defaults.
|
||||
if (
|
||||
self.config.workspace_base is None
|
||||
and self.config.runtime
|
||||
and self.config.runtime.lower() == 'local'
|
||||
):
|
||||
env_base = os.getenv('LOCAL_WORKSPACE_BASE')
|
||||
if env_base:
|
||||
self.config.workspace_base = os.path.abspath(env_base)
|
||||
else:
|
||||
self.config.workspace_base = os.path.abspath(
|
||||
os.path.join(os.getcwd(), 'workspace', 'local')
|
||||
)
|
||||
|
||||
if self.config.workspace_base is not None:
|
||||
os.makedirs(self.config.workspace_base, exist_ok=True)
|
||||
logger.warning(
|
||||
f'Workspace base path is set to {self.config.workspace_base}. '
|
||||
'It will be used as the path for the agent to run in. '
|
||||
|
||||
Reference in New Issue
Block a user