mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a0620b21e | |||
| 3356753f79 |
+1
-1
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
### 9. Use existing Docker image
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image. Follow these steps:
|
||||
1. Set the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.13-nikolaik
|
||||
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.12-nikolaik
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -38,15 +38,15 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik
|
||||
|
||||
docker run -it --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.12
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.13-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.12-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.13-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.12-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.12 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -53,6 +53,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.12 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.13
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.12
|
||||
```
|
||||
|
||||
You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -8,6 +8,7 @@ describe("Cache", () => {
|
||||
const testTTL = 1000; // 1 second
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
@@ -15,7 +16,17 @@ describe("Cache", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("gets data from memory if not expired", () => {
|
||||
it("sets data in localStorage with expiration", () => {
|
||||
cache.set(testKey, testData, testTTL);
|
||||
const cachedEntry = JSON.parse(
|
||||
localStorage.getItem(`app_cache_${testKey}`) || "",
|
||||
);
|
||||
|
||||
expect(cachedEntry.data).toEqual(testData);
|
||||
expect(cachedEntry.expiration).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("gets data from localStorage if not expired", () => {
|
||||
cache.set(testKey, testData, testTTL);
|
||||
|
||||
expect(cache.get(testKey)).toEqual(testData);
|
||||
@@ -28,6 +39,7 @@ describe("Cache", () => {
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
|
||||
|
||||
expect(cache.get(testKey)).toBeNull();
|
||||
expect(localStorage.getItem(`app_cache_${testKey}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if cached data is expired", () => {
|
||||
@@ -35,19 +47,28 @@ describe("Cache", () => {
|
||||
|
||||
vi.advanceTimersByTime(testTTL + 1);
|
||||
expect(cache.get(testKey)).toBeNull();
|
||||
expect(localStorage.getItem(`app_cache_${testKey}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("deletes data from memory", () => {
|
||||
it("deletes data from localStorage", () => {
|
||||
cache.set(testKey, testData, testTTL);
|
||||
cache.delete(testKey);
|
||||
expect(cache.get(testKey)).toBeNull();
|
||||
|
||||
expect(localStorage.getItem(`app_cache_${testKey}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("clears all data with the app prefix from memory", () => {
|
||||
it("clears all data with the app prefix from localStorage", () => {
|
||||
cache.set(testKey, testData, testTTL);
|
||||
cache.set("anotherKey", { data: "More data" }, testTTL);
|
||||
cache.clearAll();
|
||||
expect(cache.get(testKey)).toBeNull();
|
||||
expect(cache.get("anotherKey")).toBeNull();
|
||||
|
||||
expect(localStorage.length).toBe(0);
|
||||
});
|
||||
|
||||
it("does not retrieve non-prefixed data from localStorage when clearing", () => {
|
||||
localStorage.setItem("nonPrefixedKey", "should remain");
|
||||
cache.set(testKey, testData, testTTL);
|
||||
cache.clearAll();
|
||||
expect(localStorage.getItem("nonPrefixedKey")).toBe("should remain");
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.12.3",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.12.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -120,4 +120,4 @@
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,16 +63,6 @@ export async function request(
|
||||
} catch (e) {
|
||||
onFail(`Error fetching ${url}`);
|
||||
}
|
||||
if (response?.status === 401 && !url.startsWith("/api/authenticate")) {
|
||||
await request(
|
||||
"/api/authenticate",
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
true,
|
||||
);
|
||||
return request(url, options, disableToast, returnResponse, maxRetries - 1);
|
||||
}
|
||||
if (response?.status && response?.status >= 400) {
|
||||
onFail(
|
||||
`${response.status} error while fetching ${url}: ${response?.statusText}`,
|
||||
|
||||
+24
-15
@@ -5,17 +5,26 @@ type CacheEntry<T> = {
|
||||
};
|
||||
|
||||
class Cache {
|
||||
private prefix = "app_cache_";
|
||||
|
||||
private defaultTTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
private cacheMemory: Record<string, string> = {};
|
||||
/**
|
||||
* Generate a unique key with prefix for local storage
|
||||
* @param key The key to be stored in local storage
|
||||
* @returns The unique key with prefix
|
||||
*/
|
||||
private getKey(key: CacheKey): string {
|
||||
return `${this.prefix}${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cached data from memory
|
||||
* @param key The key to be retrieved from memory
|
||||
* @returns The data stored in memory
|
||||
* Retrieve the cached data from local storage
|
||||
* @param key The key to be retrieved from local storage
|
||||
* @returns The data stored in local storage
|
||||
*/
|
||||
public get<T>(key: CacheKey): T | null {
|
||||
const cachedEntry = this.cacheMemory[key];
|
||||
const cachedEntry = localStorage.getItem(this.getKey(key));
|
||||
if (cachedEntry) {
|
||||
const { data, expiration } = JSON.parse(cachedEntry) as CacheEntry<T>;
|
||||
if (Date.now() < expiration) return data;
|
||||
@@ -26,34 +35,34 @@ class Cache {
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the data in memory with expiration
|
||||
* @param key The key to be stored in memory
|
||||
* @param data The data to be stored in memory
|
||||
* Store the data in local storage with expiration
|
||||
* @param key The key to be stored in local storage
|
||||
* @param data The data to be stored in local storage
|
||||
* @param ttl The time to live for the data in milliseconds
|
||||
* @returns void
|
||||
*/
|
||||
public set<T>(key: CacheKey, data: T, ttl = this.defaultTTL): void {
|
||||
const expiration = Date.now() + ttl;
|
||||
const entry: CacheEntry<T> = { data, expiration };
|
||||
this.cacheMemory[key] = JSON.stringify(entry);
|
||||
localStorage.setItem(this.getKey(key), JSON.stringify(entry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the data from memory
|
||||
* @param key The key to be removed from memory
|
||||
* Remove the data from local storage
|
||||
* @param key The key to be removed from local storage
|
||||
* @returns void
|
||||
*/
|
||||
public delete(key: CacheKey): void {
|
||||
delete this.cacheMemory[key];
|
||||
localStorage.removeItem(this.getKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data
|
||||
* Clear all data with the app prefix from local storage
|
||||
* @returns void
|
||||
*/
|
||||
public clearAll(): void {
|
||||
Object.keys(this.cacheMemory).forEach((key) => {
|
||||
delete this.cacheMemory[key];
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
if (key.startsWith(this.prefix)) localStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+20
-11
@@ -1,12 +1,10 @@
|
||||
import os
|
||||
|
||||
from github import Github
|
||||
from github.GithubException import GithubException
|
||||
import httpx
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.sheets_client import GoogleSheetsClient
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
GITHUB_CLIENT_ID = os.getenv('GITHUB_CLIENT_ID', '').strip()
|
||||
GITHUB_CLIENT_SECRET = os.getenv('GITHUB_CLIENT_SECRET', '').strip()
|
||||
@@ -115,13 +113,24 @@ async def get_github_user(token: str) -> str:
|
||||
github handle of the user
|
||||
"""
|
||||
logger.info('Fetching GitHub user info from token')
|
||||
try:
|
||||
g = Github(token)
|
||||
user = await call_sync_from_async(g.get_user)
|
||||
login = user.login
|
||||
headers = {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'Authorization': f'Bearer {token}',
|
||||
}
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=5.0)
|
||||
) as client:
|
||||
try:
|
||||
response = await client.get('https://api.github.com/user', headers=headers)
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Error making request to GitHub API: {str(e)}')
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
logger.info('Received response from GitHub API')
|
||||
logger.debug(f'Response status code: {response.status_code}')
|
||||
response.raise_for_status()
|
||||
user_data = response.json()
|
||||
login = user_data.get('login')
|
||||
logger.info(f'Successfully retrieved GitHub user: {login}')
|
||||
return login
|
||||
except GithubException as e:
|
||||
logger.error(f'Error making request to GitHub API: {str(e)}')
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
+18
-27
@@ -2,12 +2,10 @@ import asyncio
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from pathspec import PathSpec
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
@@ -17,7 +15,6 @@ from openhands.server.data_models.feedback import FeedbackDataModel, store_feedb
|
||||
from openhands.server.github import (
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
UserVerifier,
|
||||
authenticate_github_user,
|
||||
)
|
||||
from openhands.storage import get_file_store
|
||||
@@ -63,7 +60,7 @@ from openhands.events.serialization import event_to_dict
|
||||
from openhands.events.stream import AsyncEventStreamWrapper
|
||||
from openhands.llm import bedrock
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.server.auth.auth import get_sid_from_token, sign_token
|
||||
from openhands.server.auth import get_sid_from_token, sign_token
|
||||
from openhands.server.middleware import LocalhostCORSMiddleware, NoCacheMiddleware
|
||||
from openhands.server.session import SessionManager
|
||||
|
||||
@@ -207,21 +204,23 @@ async def attach_session(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
user_verifier = UserVerifier()
|
||||
if user_verifier.is_active():
|
||||
signed_token = request.cookies.get('github_auth')
|
||||
if not signed_token:
|
||||
# First check for auth cookie
|
||||
github_token = request.cookies.get('github_auth')
|
||||
|
||||
# If no cookie, fall back to header
|
||||
if not github_token:
|
||||
github_token = request.headers.get('X-GitHub-Token')
|
||||
# If no header token either, return error
|
||||
if not github_token:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={'error': 'Not authenticated'},
|
||||
)
|
||||
try:
|
||||
jwt.decode(signed_token, config.jwt_secret, algorithms=['HS256'])
|
||||
except Exception as e:
|
||||
logger.warning(f'Invalid token: {e}')
|
||||
# If using header token, verify with GitHub
|
||||
if not await authenticate_github_user(github_token):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={'error': 'Invalid token'},
|
||||
content={'error': 'Not authenticated'},
|
||||
)
|
||||
|
||||
if not request.headers.get('Authorization'):
|
||||
@@ -877,25 +876,17 @@ async def authenticate(request: Request):
|
||||
content={'error': 'Not authorized via GitHub waitlist'},
|
||||
)
|
||||
|
||||
# Create a signed JWT token with 1-hour expiration
|
||||
cookie_data = {
|
||||
'github_token': token,
|
||||
'exp': int(time.time()) + 3600, # 1 hour expiration
|
||||
}
|
||||
signed_token = sign_token(cookie_data, config.jwt_secret)
|
||||
|
||||
response = JSONResponse(
|
||||
status_code=status.HTTP_200_OK, content={'message': 'User authenticated'}
|
||||
)
|
||||
|
||||
# Set secure cookie with signed token
|
||||
status_code=status.HTTP_200_OK, content={'message': 'User authenticated'})
|
||||
|
||||
# Set secure cookie that expires in 1 hour
|
||||
response.set_cookie(
|
||||
key='github_auth',
|
||||
value=signed_token,
|
||||
key="github_auth",
|
||||
value=token,
|
||||
max_age=3600, # 1 hour in seconds
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite='strict',
|
||||
samesite="strict"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
Generated
+1626
-1640
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.13.0"
|
||||
version = "0.12.3"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
@@ -13,7 +13,6 @@ packages = [
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
datasets = "*"
|
||||
PyGithub = "*"
|
||||
pandas = "*"
|
||||
litellm = "^1.51.1"
|
||||
google-generativeai = "*" # To use litellm with Gemini Pro API
|
||||
|
||||
Reference in New Issue
Block a user