Skip to content

Commit

Permalink
Merge branch 'All-Hands-AI:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
RainRat authored Mar 1, 2025
2 parents 0e169ff + 2e4911d commit d516795
Show file tree
Hide file tree
Showing 18 changed files with 281 additions and 65 deletions.
2 changes: 1 addition & 1 deletion frontend/src/api/open-hands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class OpenHands {
code: string,
): Promise<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
"/api/github/callback",
"/api/keycloak/callback",
{
code,
},
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/utils/generate-github-auth-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* @returns The URL to redirect to for GitHub OAuth
*/
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
const scope = "repo,user,workflow,offline_access";
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
const redirectUri = `${requestUrl.origin}/oauth/keycloak/callback`;
const baseUrl = `${requestUrl.origin}`
.replace("https://", "")
.replace("http://", "");
const authUrl = baseUrl
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev");
const scope = "openid email profile";
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
};
56 changes: 56 additions & 0 deletions microagents/knowledge/docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: docker
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- docker
- container
---

# Docker Installation and Usage Guide

## Installation on Debian/Ubuntu Systems

To install Docker on a Debian/Ubuntu system, follow these steps:

```bash
# Update package index
sudo apt-get update

# Install prerequisites
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release

# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Set up the stable repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Update package index again
sudo apt-get update

# Install Docker Engine
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
```

## Starting Docker in Container Environments

If you're in a container environment without systemd (like this workspace), start Docker with:

```bash
# Start Docker daemon in the background
sudo dockerd > /tmp/docker.log 2>&1 &

# Wait for Docker to initialize
sleep 5
```

## Verifying Docker Installation

To verify Docker is working correctly, run the hello-world container:

```bash
sudo docker run hello-world
```

50 changes: 50 additions & 0 deletions microagents/knowledge/kubernetes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: kubernetes
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- kubernetes
- k8s
- kube
---

# Kubernetes Local Development with KIND

## KIND Installation and Setup

KIND (Kubernetes IN Docker) is a tool for running local Kubernetes clusters using Docker containers as nodes. It's designed for testing Kubernetes applications locally.

IMPORTANT: Before you proceed with installation, make sure you have docker installed locally.

### Installation

To install KIND on a Debian/Ubuntu system:

```bash
# Download KIND binary
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
# Make it executable
chmod +x ./kind
# Move to a directory in your PATH
sudo mv ./kind /usr/local/bin/
```

To install kubectl:

```bash
# Download kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
# Make it executable
chmod +x kubectl
# Move to a directory in your PATH
sudo mv ./kubectl /usr/local/bin/
```

### Creating a Cluster

Create a basic KIND cluster:

```bash
kind create cluster
```
3 changes: 3 additions & 0 deletions openhands/controller/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,10 +697,13 @@ async def _step(self) -> None:
except (ContextWindowExceededError, BadRequestError, OpenAIError) as e:
# FIXME: this is a hack until a litellm fix is confirmed
# Check if this is a nested context window error
# We have to rely on string-matching because LiteLLM doesn't consistently
# wrap the failure in a ContextWindowExceededError
error_str = str(e).lower()
if (
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or 'input length and `max_tokens` exceed context limit' in error_str
or isinstance(e, ContextWindowExceededError)
):
if self.agent.config.enable_history_truncation:
Expand Down
2 changes: 1 addition & 1 deletion openhands/core/config/sandbox_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class SandboxConfig(BaseModel):
close_delay: int = Field(default=15)
remote_runtime_resource_factor: int = Field(default=1)
enable_gpu: bool = Field(default=False)
docker_runtime_kwargs: str | None = Field(default=None)
docker_runtime_kwargs: dict | None = Field(default=None)
selected_repo: str | None = Field(default=None)

model_config = {'extra': 'forbid'}
Expand Down
42 changes: 39 additions & 3 deletions openhands/core/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
import traceback
from datetime import datetime
from types import TracebackType
from typing import Any, Literal, Mapping
from typing import Any, Literal, Mapping, TextIO

import litellm
from pythonjsonlogger.json import JsonFormatter
from termcolor import colored

LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
DEBUG_LLM = os.getenv('DEBUG_LLM', 'False').lower() in ['true', '1', 'yes']

# Structured logs with JSON, disabled by default
LOG_JSON = os.getenv('LOG_JSON', 'False').lower() in ['true', '1', 'yes']
LOG_JSON_LEVEL_KEY = os.getenv('LOG_JSON_LEVEL_KEY', 'level')


# Configure litellm logging based on DEBUG_LLM
if DEBUG_LLM:
confirmation = input(
Expand Down Expand Up @@ -294,10 +300,36 @@ def get_file_handler(
file_name = f'openhands_{timestamp}.log'
file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
file_handler.setLevel(log_level)
file_handler.setFormatter(file_formatter)
if LOG_JSON:
file_handler.setFormatter(json_formatter())
else:
file_handler.setFormatter(file_formatter)
return file_handler


def json_formatter():
return JsonFormatter(
'{message}{levelname}',
style='{',
rename_fields={'levelname': LOG_JSON_LEVEL_KEY},
timestamp=True,
)


def json_log_handler(
level: int = logging.INFO,
_out: TextIO = sys.stdout,
) -> logging.Handler:
"""
Configure logger instance for structured logging as json lines.
"""

handler = logging.StreamHandler(_out)
handler.setLevel(level)
handler.setFormatter(json_formatter())
return handler


# Set up logging
logging.basicConfig(level=logging.ERROR)

Expand Down Expand Up @@ -335,7 +367,11 @@ def log_uncaught_exceptions(
LOG_TO_FILE = True
openhands_logger.debug('DEBUG mode enabled.')

openhands_logger.addHandler(get_console_handler(current_log_level))
if LOG_JSON:
openhands_logger.addHandler(json_log_handler(current_log_level))
else:
openhands_logger.addHandler(get_console_handler(current_log_level))

openhands_logger.addFilter(SensitiveDataFilter(openhands_logger.name))
openhands_logger.propagate = False
openhands_logger.debug('Logging initialized')
Expand Down
7 changes: 6 additions & 1 deletion openhands/integrations/github/github_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ class GitHubService:
token: SecretStr = SecretStr('')
refresh = False

def __init__(self, user_id: str | None = None, token: SecretStr | None = None):
def __init__(
self,
user_id: str | None = None,
idp_token: SecretStr | None = None,
token: SecretStr | None = None,
):
self.user_id = user_id

if token:
Expand Down
13 changes: 9 additions & 4 deletions openhands/runtime/impl/docker/docker_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,18 +319,23 @@ def _attach_to_container(self):
self.container = self.docker_client.containers.get(self.container_name)
if self.container.status == 'exited':
self.container.start()

config = self.container.attrs['Config']
for env_var in config['Env']:
if env_var.startswith('port='):
self._host_port = int(env_var.split('port=')[1])
self._container_port = self._host_port
elif env_var.startswith('VSCODE_PORT='):
self._vscode_port = int(env_var.split('VSCODE_PORT=')[1])

self._app_ports = []
for exposed_port in config['ExposedPorts'].keys():
exposed_port = int(exposed_port.split('/tcp')[0])
if exposed_port != self._host_port and exposed_port != self._vscode_port:
self._app_ports.append(exposed_port)
exposed_ports = config.get('ExposedPorts')
if exposed_ports:
for exposed_port in exposed_ports.keys():
exposed_port = int(exposed_port.split('/tcp')[0])
if exposed_port != self._host_port and exposed_port != self._vscode_port:
self._app_ports.append(exposed_port)

self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._container_port}'
self.log(
'debug',
Expand Down
4 changes: 4 additions & 0 deletions openhands/server/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ def get_github_token(request: Request) -> SecretStr | None:

def get_user_id(request: Request) -> str | None:
return getattr(request.state, 'github_user_id', None)


def get_idp_token(request: Request) -> SecretStr | None:
return getattr(request.state, 'idp_token', None)
44 changes: 7 additions & 37 deletions openhands/server/listen_socket.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from urllib.parse import parse_qs

import jwt
from pydantic import SecretStr
from socketio.exceptions import ConnectionRefusedError

from openhands.core.logger import openhands_logger as logger
Expand All @@ -15,18 +13,18 @@
from openhands.events.serialization import event_to_dict
from openhands.events.stream import AsyncEventStreamWrapper
from openhands.server.shared import (
ConversationStoreImpl,
SettingsStoreImpl,
config,
conversation_manager,
server_config,
sio,
)
from openhands.server.types import AppMode
from openhands.storage.conversation.conversation_validator import (
ConversationValidatorImpl,
)


@sio.event
async def connect(connection_id: str, environ, auth):
async def connect(connection_id: str, environ):
logger.info(f'sio:connect: {connection_id}')
query_params = parse_qs(environ.get('QUERY_STRING', ''))
latest_event_id = int(query_params.get('latest_event_id', [-1])[0])
Expand All @@ -35,37 +33,9 @@ async def connect(connection_id: str, environ, auth):
logger.error('No conversation_id in query params')
raise ConnectionRefusedError('No conversation_id in query params')

user_id = None
if server_config.app_mode != AppMode.OSS:
cookies_str = environ.get('HTTP_COOKIE', '')
cookies = dict(cookie.split('=', 1) for cookie in cookies_str.split('; '))
signed_token = cookies.get('openhands_auth', '')
if not signed_token:
logger.error('No openhands_auth cookie')
raise ConnectionRefusedError('No openhands_auth cookie')
if not config.jwt_secret:
raise RuntimeError('JWT secret not found')

jwt_secret = (
config.jwt_secret.get_secret_value()
if isinstance(config.jwt_secret, SecretStr)
else config.jwt_secret
)
decoded = jwt.decode(signed_token, jwt_secret, algorithms=['HS256'])
user_id = decoded['github_user_id']

logger.info(f'User {user_id} is connecting to conversation {conversation_id}')

conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
metadata = await conversation_store.get_metadata(conversation_id)

if metadata.github_user_id != str(user_id):
logger.error(
f'User {user_id} is not allowed to join conversation {conversation_id}'
)
raise ConnectionRefusedError(
f'User {user_id} is not allowed to join conversation {conversation_id}'
)
cookies_str = environ.get('HTTP_COOKIE', '')
conversation_validator = ConversationValidatorImpl()
user_id = await conversation_validator.validate(conversation_id, cookies_str)

settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
Expand Down
Loading

0 comments on commit d516795

Please sign in to comment.