Skip to content

Latest commit

 

History

History
827 lines (712 loc) · 23.8 KB

File metadata and controls

827 lines (712 loc) · 23.8 KB

Слой Server/API — HTTP API и сессии

Обзор модуля

Модуль openhands/server/ реализует HTTP API на FastAPI с WebSocket (Socket.IO) для реального времени. Включает маршруты, управление сессиями, разговорами, аутентификацию, middleware и мониторинг.

Архитектура сервера

graph TB
    subgraph "FastAPI Application"
        direction TB
        HEALTH["Health Routes"]
        PUBLIC["Public Routes"]
        CONV["Conversation Routes"]
        MGMT["Management Routes"]
        FILES["File Routes"]
        SETTINGS["Settings Routes"]
        SECRETS["Secrets Routes"]
        GIT["Git Routes"]
        SECURITY["Security Routes"]
        TRAJ["Trajectory Routes"]
        FEEDBACK["Feedback Routes"]
        MCP_R["MCP Routes"]
    end

    subgraph "Middleware"
        CORS["LocalhostCORSMiddleware"]
        CACHE["CacheControlMiddleware"]
        RATE["RateLimitMiddleware"]
    end

    subgraph "Session Management"
        CONV_MGR["ConversationManager"]
        STANDALONE["StandaloneConversationManager"]
        DOCKER_NESTED["DockerNestedConversationManager"]
        WEB_SESSION["WebSession"]
        AGENT_SESSION["AgentSession"]
        SRV_CONV["ServerConversation"]
    end

    subgraph "Auth"
        UA["UserAuth"]
        DEFAULT_UA["DefaultUserAuth"]
    end

    subgraph "Monitoring"
        MON["MonitoringListener"]
    end

    CLIENT["Client (Browser/CLI)"] --> CORS --> CACHE --> RATE
    RATE --> HEALTH
    RATE --> PUBLIC
    RATE --> CONV
    RATE --> MGMT
    RATE --> FILES
    RATE --> SETTINGS
    RATE --> SECRETS
    RATE --> GIT
    RATE --> SECURITY
    RATE --> TRAJ
    RATE --> FEEDBACK
    RATE --> MCP_R

    CONV --> CONV_MGR
    MGMT --> CONV_MGR
    FILES --> CONV_MGR
    CONV_MGR --> WEB_SESSION
    WEB_SESSION --> AGENT_SESSION
    AGENT_SESSION --> SRV_CONV

    SETTINGS --> UA
    SECRETS --> UA
    GIT --> UA
Loading

ConversationManager

classDiagram
    class ConversationManager {
        <<abstract>>
        +attach_to_conversation(sid, user_id)* ServerConversation
        +detach_from_conversation(conversation)*
        +join_conversation(sid, connection_id, settings, user_id)* AgentLoopInfo
        +is_agent_loop_running(sid)* bool
        +get_running_agent_loops(user_id, filter_to_sids)* set~str~
        +get_connections(user_id, filter_to_sids)* dict
        +maybe_start_agent_loop(sid, settings, user_id, initial_user_msg, replay_json)* AgentLoopInfo
        +send_to_event_stream(connection_id, data)*
        +send_event_to_conversation(sid, data)*
        +disconnect_from_session(connection_id)*
        +close_session(sid)*
        +get_agent_session(sid)* AgentSession
        +get_agent_loop_info(user_id, filter_to_sids)* list~AgentLoopInfo~
        +request_llm_completion(sid, service_id, llm_config, messages)* str
        +list_files(sid, path)* list~str~
        +select_file(sid, file)* tuple
        +upload_files(sid, files)* tuple
        +get_instance(...)$ ConversationManager
    }

    class StandaloneConversationManager {
        +attach_to_conversation(sid, user_id) ServerConversation
        +detach_from_conversation(conversation)
        +join_conversation(sid, connection_id, settings, user_id) AgentLoopInfo
        +get_running_agent_loops(user_id, filter_to_sids) set~str~
        +get_connections(user_id, filter_to_sids) dict
        +maybe_start_agent_loop(sid, settings, user_id, ...) AgentLoopInfo
        +send_to_event_stream(connection_id, data)
        +send_event_to_conversation(sid, data)
        +disconnect_from_session(connection_id)
        +close_session(sid)
        +get_agent_session(sid) AgentSession
        +get_agent_loop_info(user_id, ...) list~AgentLoopInfo~
        +request_llm_completion(...) str
        +list_files(sid, path) list~str~
        +select_file(sid, file) tuple
        +upload_files(sid, files) tuple
    }

    class DockerNestedConversationManager {
        +get_running_agent_loops(user_id, filter_to_sids) set~str~
        +maybe_start_agent_loop(sid, settings, ...) AgentLoopInfo
        +send_event_to_conversation(sid, data)
        +close_session(sid)
        +get_agent_loop_info(user_id, ...) list~AgentLoopInfo~
        +list_files(sid, path) list~str~
        +select_file(sid, file) tuple
        +upload_files(sid, files) tuple
    }

    ConversationManager <|-- StandaloneConversationManager
    ConversationManager <|-- DockerNestedConversationManager
Loading

Session Management

classDiagram
    class WebSession {
        -str sid
        -OpenHandsConfig config
        -LLMRegistry llm_registry
        -ConversationStats conversation_stats
        -FileStore file_store
        -AsyncServer sio
        +close()
        +initialize_agent(settings, initial_message, replay_json)
        +on_event(event: Event)
        +dispatch(data: dict)
        +send(data: dict)
        +send_error(message: str)
        +queue_status_message(msg_type, runtime_status, message)
    }

    class AgentSession {
        -str sid
        -FileStore file_store
        -LLMRegistry llm_registry
        -ConversationStats conversation_stats
        +start(runtime_name, config, agent, max_iterations, ...)
        +close()
        +get_state() AgentState
        +is_closed() bool
        +override_provider_tokens_with_custom_secret(...)
    }

    class ServerConversation {
        -str sid
        -FileStore file_store
        -OpenHandsConfig config
        -EventStream event_stream
        -Runtime runtime
        +connect()
        +disconnect()
        +security_analyzer
    }

    class ConversationInitData {
        inherits Settings
    }

    WebSession --> AgentSession : manages
    AgentSession --> ServerConversation : uses
Loading

Routes — Маршруты API

graph LR
    subgraph "Health"
        GET_ALIVE["GET /alive"]
        GET_HEALTH["GET /health"]
        GET_INFO["GET /info"]
        GET_READY["GET /ready"]
    end

    subgraph "Public"
        GET_MODELS["GET /api/litellm-models"]
        GET_AGENTS["GET /api/agents"]
        GET_ANALYZERS["GET /api/security-analyzers"]
        GET_CONFIG["GET /api/config"]
    end

    subgraph "Conversations"
        POST_CONV["POST /api/conversations"]
        GET_CONVS["GET /api/conversations"]
        GET_CONV["GET /api/conversations/{id}"]
        DEL_CONV["DELETE /api/conversations/{id}"]
        POST_START["POST /api/conversations/{id}/start"]
        POST_STOP["POST /api/conversations/{id}/stop"]
        PUT_CONV["PUT /api/conversations/{id}"]
        GET_EVENTS["GET /api/conversations/{id}/events"]
        POST_EVENT["POST /api/conversations/{id}/events"]
        POST_MSG["POST /api/conversations/{id}/messages"]
    end

    subgraph "Files"
        GET_FILES["GET /api/conversations/{id}/list-files"]
        GET_FILE["GET /api/conversations/{id}/select-file"]
        GET_ZIP["GET /api/conversations/{id}/zip-directory"]
        POST_UPLOAD["POST /api/conversations/{id}/upload-files"]
        GET_CHANGES["GET /api/conversations/{id}/git-changes"]
        GET_DIFF["GET /api/conversations/{id}/git-diff"]
    end

    subgraph "Settings"
        GET_SETTINGS["GET /api/settings"]
        POST_SETTINGS["POST /api/settings"]
        POST_RESET["POST /api/reset-settings"]
    end

    subgraph "Secrets"
        GET_SECRETS["GET /api/secrets"]
        POST_SECRET["POST /api/secrets"]
        PUT_SECRET["PUT /api/secrets/{name}"]
        DEL_SECRET["DELETE /api/secrets/{name}"]
    end

    subgraph "Git"
        GET_REPOS["GET /api/user/repositories"]
        GET_USER["GET /api/user"]
        GET_SEARCH["GET /api/user/search"]
        GET_BRANCHES["GET /api/user/branches"]
        GET_TASKS["GET /api/user/suggested-tasks"]
    end
Loading

Middleware

classDiagram
    class LocalhostCORSMiddleware {
        +is_allowed_origin(origin: str) bool
    }

    class CacheControlMiddleware {
        +dispatch(request, call_next) Response
    }

    class InMemoryRateLimiter {
        -int requests
        -int seconds
        -int sleep_seconds
        +__call__(request: Request) bool
    }

    class RateLimitMiddleware {
        -InMemoryRateLimiter rate_limiter
        +dispatch(request, call_next) Response
        +is_rate_limited_request(request) bool
    }

    class UserAuth {
        <<abstract>>
        +get_user_id()* str
        +get_user_email()* str
        +get_access_token()* SecretStr
        +get_provider_tokens()* PROVIDER_TOKEN_TYPE
        +get_user_settings_store()* SettingsStore
        +get_user_settings()* Settings
        +get_secrets_store()* SecretsStore
        +get_secrets()* Secrets
        +get_auth_type()* AuthType
        +get_mcp_api_key()* str
        +get_instance(request)$ UserAuth
        +get_for_user(user_id)$ UserAuth
    }

    class DefaultUserAuth {
        +get_user_id() str
        +get_user_email() str
        +get_access_token() SecretStr
        +get_user_settings_store() SettingsStore
        +get_user_settings() Settings
        +get_secrets_store() SecretsStore
        +get_secrets() Secrets
        +get_provider_tokens() PROVIDER_TOKEN_TYPE
        +get_mcp_api_key() str
    }

    UserAuth <|-- DefaultUserAuth
    RateLimitMiddleware --> InMemoryRateLimiter
Loading

Data Models

classDiagram
    class ConversationInfo {
        <<dataclass>>
        +str conversation_id
        +str title
        +str last_updated_at
        +str status
        +str runtime_status
        +str selected_repository
        +str selected_branch
        +str git_provider
        +str trigger
        +int num_connections
        +str url
        +str session_api_key
        +str created_at
        +int pr_number
        +str conversation_version
        +list sub_conversation_ids
        +bool public
        +str sandbox_id
    }

    class ConversationInfoResultSet {
        <<dataclass>>
        +list~ConversationInfo~ results
        +str next_page_id
    }

    class AgentLoopInfo {
        <<dataclass>>
        +str conversation_id
        +str url
        +str session_api_key
        +EventStore event_store
        +str status
        +str runtime_status
    }

    class ConversationStats {
        -FileStore file_store
        -str conversation_id
        +save_metrics()
        +maybe_restore_metrics()
        +get_combined_metrics() Metrics
        +get_metrics_for_service(service_id) Metrics
        +register_llm(event: RegistryEvent)
        +merge_and_save(conversation_stats)
    }

    ConversationInfoResultSet o-- ConversationInfo
Loading

App Server V1 — Расширенный серверный слой

Модуль openhands/app_server/ — это серверный слой нового поколения (v1), построенный поверх FastAPI с Dependency Injection через Injector-паттерн, SQL-хранилищем и Docker/Remote sandbox управлением.

Архитектура App Server V1

graph TB
    subgraph "v1_router (prefix=/api/v1)"
        ER["event_router<br>/conversation/{id}/events"]
        ACR["app_conversation_router<br>/app-conversations"]
        SR["sandbox_router<br>/sandboxes"]
        SSR["sandbox_spec_router<br>/sandbox-specs"]
        UR["user_router<br>/users"]
        WR["webhook_router<br>/webhooks"]
        WCR["web_client_router<br>/web-client"]
    end

    subgraph "Services (Abstract → Concrete)"
        ES["EventService"]
        ECS["EventCallbackService"]
        SS["SandboxService"]
        SSS["SandboxSpecService"]
        ACS["AppConversationService"]
        ACIS["AppConversationInfoService"]
        ACTS["AppConversationStartTaskService"]
        JS["JwtService"]
        UC["UserContext"]
    end

    subgraph "SQL Storage"
        DB["SQLite / PostgreSQL / GCP CloudSQL"]
        ALE["Alembic Migrations (001-006)"]
    end

    ER --> ES
    ACR --> ACS
    ACR --> ACTS
    SR --> SS
    SSR --> SSS
    UR --> UC
    WR --> ECS
    WCR --> WCC["WebClientConfig"]

    ACS --> ACIS
    ACS --> ACTS
    ACS --> ECS
    ACS --> SS
    ACS --> SSS

    ES --> DB
    ECS --> DB
    ACIS --> DB
    ACTS --> DB
    SS --> DB
    ALE --> DB
Loading

Dependency Injection Framework

classDiagram
    class Injector~T~ {
        <<abstract>>
        +inject(state: InjectorState, request: Request)* AsyncGenerator~T~
        +context(state: InjectorState, request: Request) AsyncGenerator~T~
        +depends(request: Request) AsyncGenerator~T~
    }

    class InjectorState {
        <<TypeAlias>>
        State
    }

    class DbSessionInjector {
        +Path persistence_dir
        +str host
        +int port
        +str name
        +str user
        +SecretStr password
        +bool echo = False
        +int pool_size = 25
        +int max_overflow = 10
        +int pool_recycle = 1800
        +str gcp_db_instance
        +str gcp_project
        +str gcp_region
        +get_async_db_engine() AsyncEngine
        +get_db_engine() Engine
        +get_session_maker() sessionmaker
        +get_async_session_maker() async_sessionmaker
        +async_session() AsyncGenerator~AsyncSession~
    }

    class JwtServiceInjector {
        +Path persistence_dir
        +get_jwt_service() JwtService
    }

    class HttpxClientInjector {
        +int timeout = 15
    }

    Injector <|-- DbSessionInjector
    Injector <|-- JwtServiceInjector
    Injector <|-- HttpxClientInjector
Loading

Event Service

classDiagram
    class EventService {
        <<abstract>>
        +get_event(conversation_id: UUID, event_id: UUID)* Event|None
        +search_events(conversation_id, kind__eq, timestamp__gte, timestamp__lt, sort_order, page_id, limit)* EventPage
        +count_events(conversation_id, kind__eq, timestamp__gte, timestamp__lt)* int
        +save_event(conversation_id: UUID, event: Event)*
        +batch_get_events(conversation_id: UUID, event_ids: list~UUID~) list~Event|None~
    }

    class EventServiceBase {
        <<abstract>>
        +Path prefix
        +str user_id
        +AppConversationInfoService app_conversation_info_service
        +get_conversation_path(conversation_id: UUID) Path
        #_load_event(path: Path)* Event|None
        #_store_event(path: Path, event: Event)*
        #_search_paths(prefix: Path)* list~Path~
    }

    class FilesystemEventService {
        +int limit = 500
    }

    class GoogleCloudEventService {
        +Bucket bucket
    }

    EventService <|-- EventServiceBase
    EventServiceBase <|-- FilesystemEventService
    EventServiceBase <|-- GoogleCloudEventService
Loading

Sandbox Service

classDiagram
    class SandboxService {
        <<abstract>>
        +search_sandboxes(page_id, limit)* SandboxPage
        +get_sandbox(sandbox_id: str)* SandboxInfo|None
        +get_sandbox_by_session_api_key(session_api_key: str)* SandboxInfo|None
        +batch_get_sandboxes(sandbox_ids: list~str~) list~SandboxInfo|None~
        +start_sandbox(sandbox_spec_id, sandbox_id)* SandboxInfo
        +resume_sandbox(sandbox_id: str)* bool
        +wait_for_sandbox_running(sandbox_id, timeout, poll_interval, httpx_client) SandboxInfo
        +pause_sandbox(sandbox_id: str)* bool
        +delete_sandbox(sandbox_id: str)* bool
        +pause_old_sandboxes(max_num_sandboxes: int) list~str~
    }

    class DockerSandboxService {
        +SandboxSpecService sandbox_spec_service
        +str container_name_prefix
        +int host_port
        +str container_url_pattern
        +list~VolumeMount~ mounts
        +list~ExposedPort~ exposed_ports
        +int max_num_sandboxes
        +bool use_host_network
    }

    class ProcessSandboxService {
        +str base_working_dir
        +int base_port
        +str python_executable
        +str agent_server_module
    }

    class RemoteSandboxService {
        +str api_url
        +str api_key
        +int resource_factor
        +str runtime_class
        +int start_sandbox_timeout
        +int max_num_sandboxes
        +AsyncSession db_session
    }

    class SandboxStatus {
        <<enumeration>>
        STARTING
        RUNNING
        PAUSED
        ERROR
        MISSING
    }

    class SandboxInfo {
        +str id
        +SandboxStatus status
        +list~ExposedUrl~ exposed_urls
    }

    SandboxService <|-- DockerSandboxService
    SandboxService <|-- ProcessSandboxService
    SandboxService <|-- RemoteSandboxService
    SandboxService ..> SandboxInfo
    SandboxInfo ..> SandboxStatus
Loading

App Conversation Service

classDiagram
    class AppConversationService {
        <<abstract>>
        +search_app_conversations(title__contains, created_at__gte, created_at__lt, updated_at__gte, updated_at__lt, sort_order, page_id, limit, include_sub_conversations)* AppConversationPage
        +count_app_conversations(...)* int
        +get_app_conversation(conversation_id: UUID)* AppConversation|None
        +batch_get_app_conversations(conversation_ids) list
        +start_app_conversation(request: AppConversationStartRequest)* AsyncGenerator~AppConversationStartTask~
        +update_app_conversation(conversation_id: UUID, request: AppConversationUpdateRequest)* AppConversation|None
        +delete_app_conversation(conversation_id: UUID)* bool
        +export_conversation(conversation_id: UUID)* bytes
    }

    class AppConversationServiceBase {
        <<abstract>>
        +bool init_git_in_empty_workspace
        +UserContext user_context
        +load_and_merge_all_skills(sandbox, selected_repository, working_dir, agent_server_url) list~Skill~
        +run_setup_scripts(task, sandbox, workspace, agent_server_url)
        +clone_or_init_git_repo(task, workspace)
        +maybe_run_setup_script(workspace)
        +maybe_setup_git_hooks(workspace)
    }

    class LiveStatusAppConversationService {
        +UserContext user_context
        +AppConversationInfoService app_conversation_info_service
        +AppConversationStartTaskService app_conversation_start_task_service
        +EventCallbackService event_callback_service
        +EventService event_service
        +SandboxService sandbox_service
        +SandboxSpecService sandbox_spec_service
        +JwtService jwt_service
        +int sandbox_startup_timeout
        +httpx.AsyncClient httpx_client
        +str web_url
        +str openhands_provider_base_url
    }

    class AppConversationStartTaskStatus {
        <<enumeration>>
        WORKING
        WAITING_FOR_SANDBOX
        PREPARING_REPOSITORY
        RUNNING_SETUP_SCRIPT
        SETTING_UP_GIT_HOOKS
        SETTING_UP_SKILLS
        STARTING_CONVERSATION
        READY
        ERROR
    }

    AppConversationService <|-- AppConversationServiceBase
    AppConversationServiceBase <|-- LiveStatusAppConversationService
Loading

Event Callback Service

classDiagram
    class EventCallbackService {
        <<abstract>>
        +create_event_callback(request: CreateEventCallbackRequest)* EventCallback
        +get_event_callback(id: UUID)* EventCallback|None
        +delete_event_callback(id: UUID)* bool
        +search_event_callbacks(conversation_id__eq, event_kind__eq, event_id__eq, page_id, limit)* EventCallbackPage
        +save_event_callback(event_callback: EventCallback)* EventCallback
        +execute_callbacks(conversation_id: UUID, event: Event)*
    }

    class EventCallbackProcessor {
        <<abstract>>
        +__call__(conversation_id: UUID, callback: EventCallback, event: Event)* EventCallbackResult|None
    }

    class LoggingCallbackProcessor {
        +__call__(conversation_id, callback, event) EventCallbackResult
    }

    class SetTitleCallbackProcessor {
        +__call__(conversation_id, callback, event) EventCallbackResult|None
    }

    class SQLEventCallbackService {
        +AsyncSession db_session
        +execute_callback(conversation_id, callback, event)
    }

    class EventCallbackStatus {
        <<enumeration>>
        ACTIVE
        DISABLED
        COMPLETED
        ERROR
    }

    EventCallbackService <|-- SQLEventCallbackService
    EventCallbackProcessor <|-- LoggingCallbackProcessor
    EventCallbackProcessor <|-- SetTitleCallbackProcessor
Loading

User Context & Auth

classDiagram
    class UserContext {
        <<abstract>>
        +get_user_id()* str|None
        +get_user_info()* UserInfo
        +get_authenticated_git_url(repository: str, is_optional: bool)* str
        +get_provider_tokens()* PROVIDER_TOKEN_TYPE|None
        +get_latest_token(provider_type: ProviderType)* str|None
        +get_secrets()* dict~str,SecretSource~
        +get_mcp_api_key()* str|None
    }

    class AuthUserContext {
        +UserAuth user_auth
        +get_provider_handler() ProviderHandler
    }

    class SpecifyUserContext {
        +str user_id
    }

    class JwtService {
        +list~EncryptionKey~ keys
        +str default_key_id
        +create_jws_token(payload: dict, key_id, expires_in) str
        +verify_jws_token(token: str, key_id) dict
        +create_jwe_token(payload: dict, key_id, expires_in) str
        +decrypt_jwe_token(token: str, key_id) dict
    }

    UserContext <|-- AuthUserContext
    UserContext <|-- SpecifyUserContext
Loading

Web Client Config

classDiagram
    class WebClientConfig {
        +WebClientFeatureFlags feature_flags
        +list~ProviderType~ providers_configured
        +str posthog_client_key
        +datetime maintenance_start_time
        +str auth_url
        +str recaptcha_site_key
        +list~str~ faulty_models
        +str error_message
        +str github_app_slug
    }

    class WebClientFeatureFlags {
        <<fields from env vars>>
    }

    class DefaultWebClientConfigInjector {
        +get_web_client_config() WebClientConfig
    }

    WebClientConfig o-- WebClientFeatureFlags
    DefaultWebClientConfigInjector ..> WebClientConfig
Loading

Sequence: Создание Conversation (V1 Flow)

sequenceDiagram
    participant Client
    participant Router as app_conversation_router
    participant ACS as LiveStatusAppConversationService
    participant SS as SandboxService
    participant ACTS as StartTaskService
    participant ECS as EventCallbackService
    participant DB as Database

    Client->>Router: POST /api/v1/app-conversations (start)
    Router->>ACS: start_app_conversation(request)
    ACS->>SS: start_sandbox(sandbox_spec_id)
    SS->>SS: create container / remote runtime
    SS-->>ACS: SandboxInfo (status=STARTING)
    ACS->>ACTS: save_app_conversation_start_task(task)
    ACTS->>DB: INSERT app_conversation_start_task
    
    loop Wait for sandbox
        ACS->>SS: get_sandbox(sandbox_id)
        SS-->>ACS: SandboxInfo (status check)
    end

    ACS->>ACS: clone_or_init_git_repo()
    ACS->>ACS: run_setup_scripts()
    ACS->>ACS: load_and_merge_all_skills()
    ACS->>ACS: _build_start_conversation_request_for_user()
    ACS->>ECS: create_event_callback(set_title)
    ACS-->>Router: yield AppConversationStartTask (READY)
    Router-->>Client: SSE stream / JSON response
Loading

Alembic Migrations

graph LR
    subgraph "Database Migrations"
        M1["001: Initial tables<br>- app_conversation_start_task<br>- event_callback<br>- event_callback_result<br>- v1_remote_sandbox<br>- conversation_metadata"]
        M2["002: Add status/updated_at<br>to event_callback,<br>change event_id type"]
        M3["003: Add parent_conversation_id<br>to conversation_metadata"]
        M5["005: Remove github_user_id,<br>make user_id nullable"]
        M6["006: Add session_api_key_hash<br>to v1_remote_sandbox"]
    end

    M1 --> M2 --> M3 --> M5 --> M6
Loading

App Server Error Hierarchy

classDiagram
    class HTTPException {
        <<fastapi>>
    }

    class OpenHandsError {
        +Any detail
        +dict headers
        +int status_code = 500
    }

    class AuthError {
        +int status_code = 401
    }

    class PermissionsError {
        +int status_code = 403
    }

    class SandboxError {
        +int status_code = 500
    }

    HTTPException <|-- OpenHandsError
    OpenHandsError <|-- AuthError
    OpenHandsError <|-- PermissionsError
    OpenHandsError <|-- SandboxError
Loading