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