mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Add the ability for API callers to pass secrets directly when starting a conversation, without requiring them to be pre-stored in the database. Changes: - Add optional `secrets: dict[str, SecretStr]` field to AppConversationStartRequest model - Update `_build_start_conversation_request_for_user()` to merge API-provided secrets with existing secrets (from git providers/database) - API-provided secrets take precedence over existing secrets with same name - Add new `openhands/app_server/constants.py` with secret validation: - Blocked names: container config vars (OH_*, WORKER_*, etc.) - Blocked prefixes: LLM_* (to enforce app-server LLM controls) - Configurable size limits via environment variables - Add warning log when API secrets override existing secrets - Bump agent-server image to 1.18.1-python (SDK v1.18.1 with MCP secrets expansion support) Closes #14007
170 lines
6.6 KiB
Python
170 lines
6.6 KiB
Python
"""Constants for the OpenHands App Server.
|
|
|
|
This module contains constants that are used across the app server,
|
|
including security-related configurations for secret name validation.
|
|
"""
|
|
|
|
import os
|
|
from collections.abc import Mapping
|
|
|
|
# =============================================================================
|
|
# SECRET LIMITS (configurable via environment variables)
|
|
# =============================================================================
|
|
|
|
# Maximum number of secrets that can be passed via API in a single request.
|
|
# Prevents abuse by limiting the size of the secrets dictionary.
|
|
# Override with: OH_MAX_API_SECRETS_COUNT
|
|
MAX_API_SECRETS_COUNT: int = int(os.getenv('OH_MAX_API_SECRETS_COUNT', '50'))
|
|
|
|
# Maximum length of a secret name in characters.
|
|
# Environment variable names should be concise; this prevents excessively long names.
|
|
# Override with: OH_MAX_API_SECRET_NAME_LENGTH
|
|
MAX_API_SECRET_NAME_LENGTH: int = int(os.getenv('OH_MAX_API_SECRET_NAME_LENGTH', '256'))
|
|
|
|
# Maximum length of a secret value in bytes.
|
|
# 64KB is generous for API keys/tokens while preventing massive payloads.
|
|
# Override with: OH_MAX_API_SECRET_VALUE_LENGTH
|
|
MAX_API_SECRET_VALUE_LENGTH: int = int(
|
|
os.getenv('OH_MAX_API_SECRET_VALUE_LENGTH', '65536')
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# SECRET NAME VALIDATION
|
|
# =============================================================================
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# BLOCKED: These names CANNOT be used as user-provided secrets.
|
|
#
|
|
# These environment variables are injected into the agent-server container
|
|
# at startup. User-provided secrets with these names would override them
|
|
# when exported in bash commands, potentially breaking the sandbox or
|
|
# creating security vulnerabilities.
|
|
# -----------------------------------------------------------------------------
|
|
BLOCKED_SECRET_NAMES: frozenset[str] = frozenset(
|
|
{
|
|
# Agent-server container configuration (from initial_env)
|
|
'OPENVSCODE_SERVER_ROOT',
|
|
'OH_ENABLE_VNC',
|
|
'LOG_JSON',
|
|
'OH_CONVERSATIONS_PATH',
|
|
'OH_BASH_EVENTS_DIR',
|
|
'PYTHONUNBUFFERED',
|
|
'ENV_LOG_LEVEL',
|
|
# Webhook and CORS - overriding could redirect callbacks to malicious endpoints
|
|
'OH_WEBHOOKS_0_BASE_URL',
|
|
'OH_ALLOW_CORS_ORIGINS_0',
|
|
# Worker ports - could break web application functionality
|
|
'WORKER_1',
|
|
'WORKER_2',
|
|
}
|
|
)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# BLOCKED PREFIXES: Secret names starting with these prefixes are blocked.
|
|
#
|
|
# LLM_* variables are auto-forwarded to the agent-server container to enforce
|
|
# LLM controls (timeouts, retries, model restrictions, etc.). Allowing users
|
|
# to override these would let them escape app-server LLM controls.
|
|
# -----------------------------------------------------------------------------
|
|
BLOCKED_SECRET_PREFIXES: tuple[str, ...] = ('LLM_',)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# OVERRIDABLE: These are system-provided but users MAY override them.
|
|
# Documented here for clarity - these are explicitly ALLOWED, not blocked.
|
|
#
|
|
# Use case: User wants to use their own credentials instead of the
|
|
# organization-level credentials provided by the system.
|
|
# -----------------------------------------------------------------------------
|
|
OVERRIDABLE_SYSTEM_SECRETS: frozenset[str] = frozenset(
|
|
{
|
|
# Git Provider Tokens - users may provide their own credentials
|
|
# Note: Provider tokens are fetched via app-server API, not container env
|
|
'GITHUB_TOKEN',
|
|
'GITLAB_TOKEN',
|
|
'BITBUCKET_TOKEN',
|
|
'AZURE_DEVOPS_TOKEN',
|
|
'FORGEJO_TOKEN',
|
|
# AWS Credentials - used for Bedrock LLM access
|
|
# Users may want to use their own AWS account for Bedrock models
|
|
'AWS_ACCESS_KEY_ID',
|
|
'AWS_SECRET_ACCESS_KEY',
|
|
'AWS_REGION_NAME',
|
|
}
|
|
)
|
|
|
|
|
|
def validate_secret_name(name: str) -> None:
|
|
"""Validate that a secret name is allowed.
|
|
|
|
Args:
|
|
name: The secret name to validate
|
|
|
|
Raises:
|
|
ValueError: If the name is blocked (exact match or prefix match),
|
|
or exceeds the maximum length
|
|
"""
|
|
# Check name length
|
|
if len(name) > MAX_API_SECRET_NAME_LENGTH:
|
|
raise ValueError(
|
|
f'Secret name exceeds maximum length of {MAX_API_SECRET_NAME_LENGTH} characters '
|
|
f'(got {len(name)}). Configure via OH_MAX_API_SECRET_NAME_LENGTH.'
|
|
)
|
|
|
|
upper_name = name.upper()
|
|
|
|
# Check exact matches
|
|
if upper_name in BLOCKED_SECRET_NAMES:
|
|
raise ValueError(
|
|
f"Secret name '{name}' is reserved for internal use and cannot be overridden. "
|
|
f'See openhands.app_server.constants for the list of blocked names.'
|
|
)
|
|
|
|
# Check prefix matches
|
|
for prefix in BLOCKED_SECRET_PREFIXES:
|
|
if upper_name.startswith(prefix):
|
|
raise ValueError(
|
|
f"Secret name '{name}' starts with reserved prefix '{prefix}' and cannot be used. "
|
|
f'These variables are used for LLM configuration controls.'
|
|
)
|
|
|
|
# Note: OVERRIDABLE_SYSTEM_SECRETS are intentionally allowed
|
|
|
|
|
|
def validate_secrets_dict(secrets: Mapping[str, object] | None) -> None:
|
|
"""Validate the entire secrets dictionary for size limits.
|
|
|
|
This should be called before iterating over individual secrets.
|
|
|
|
Args:
|
|
secrets: The secrets dictionary to validate (can be None).
|
|
Values can be str or SecretStr (uses get_secret_value()).
|
|
|
|
Raises:
|
|
ValueError: If the dictionary exceeds size limits
|
|
"""
|
|
if secrets is None:
|
|
return
|
|
|
|
# Check number of secrets
|
|
if len(secrets) > MAX_API_SECRETS_COUNT:
|
|
raise ValueError(
|
|
f'Too many secrets provided: {len(secrets)} exceeds maximum of '
|
|
f'{MAX_API_SECRETS_COUNT}. Configure via OH_MAX_API_SECRETS_COUNT.'
|
|
)
|
|
|
|
# Check individual value lengths
|
|
for name, value in secrets.items():
|
|
# Handle both str and SecretStr (Pydantic's SecretStr has get_secret_value())
|
|
if hasattr(value, 'get_secret_value'):
|
|
value_str = value.get_secret_value() # type: ignore[union-attr]
|
|
else:
|
|
value_str = str(value)
|
|
value_bytes = len(value_str.encode('utf-8'))
|
|
if value_bytes > MAX_API_SECRET_VALUE_LENGTH:
|
|
raise ValueError(
|
|
f"Secret '{name}' value exceeds maximum length of "
|
|
f'{MAX_API_SECRET_VALUE_LENGTH} bytes (got {value_bytes}). '
|
|
f'Configure via OH_MAX_API_SECRET_VALUE_LENGTH.'
|
|
)
|