APP-216 Support multiple git providers in conversation secrets (#11908)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell
2025-12-05 11:50:45 -07:00
committed by GitHub
parent 7811a62491
commit 72c7d9c497
7 changed files with 134 additions and 75 deletions

View File

@@ -526,13 +526,10 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
if not request.llm_model and parent_info.llm_model:
request.llm_model = parent_info.llm_model
async def _setup_secrets_for_git_provider(
self, git_provider: ProviderType | None, user: UserInfo
) -> dict:
"""Set up secrets for git provider authentication.
async def _setup_secrets_for_git_providers(self, user: UserInfo) -> dict:
"""Set up secrets for all git provider authentication.
Args:
git_provider: The git provider type (GitHub, GitLab, etc.)
user: User information containing authentication details
Returns:
@@ -540,35 +537,42 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
"""
secrets = await self.user_context.get_secrets()
if not git_provider:
# Get all provider tokens from user authentication
provider_tokens = await self.user_context.get_provider_tokens()
if not provider_tokens:
return secrets
secret_name = f'{git_provider.name}_TOKEN'
# Create secrets for each provider token
for provider_type, provider_token in provider_tokens.items():
if not provider_token.token:
continue
if self.web_url:
# Create an access token for web-based authentication
access_token = self.jwt_service.create_jws_token(
payload={
'user_id': user.id,
'provider_type': git_provider.value,
},
expires_in=self.access_token_hard_timeout,
)
headers = {'X-Access-Token': access_token}
secret_name = f'{provider_type.name}_TOKEN'
# Include keycloak_auth cookie in headers if app_mode is SaaS
if self.app_mode == 'saas' and self.keycloak_auth_cookie:
headers['Cookie'] = f'keycloak_auth={self.keycloak_auth_cookie}'
if self.web_url:
# Create an access token for web-based authentication
access_token = self.jwt_service.create_jws_token(
payload={
'user_id': user.id,
'provider_type': provider_type.value,
},
expires_in=self.access_token_hard_timeout,
)
headers = {'X-Access-Token': access_token}
secrets[secret_name] = LookupSecret(
url=self.web_url + '/api/v1/webhooks/secrets',
headers=headers,
)
else:
# Use static token for environments without web URL access
static_token = await self.user_context.get_latest_token(git_provider)
if static_token:
secrets[secret_name] = StaticSecret(value=static_token)
# Include keycloak_auth cookie in headers if app_mode is SaaS
if self.app_mode == 'saas' and self.keycloak_auth_cookie:
headers['Cookie'] = f'keycloak_auth={self.keycloak_auth_cookie}'
secrets[secret_name] = LookupSecret(
url=self.web_url + '/api/v1/webhooks/secrets',
headers=headers,
)
else:
# Use static token for environments without web URL access
static_token = await self.user_context.get_latest_token(provider_type)
if static_token:
secrets[secret_name] = StaticSecret(value=static_token)
return secrets
@@ -768,8 +772,8 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
user = await self.user_context.get_user_info()
workspace = LocalWorkspace(working_dir=working_dir)
# Set up secrets for git provider
secrets = await self._setup_secrets_for_git_provider(git_provider, user)
# Set up secrets for all git providers
secrets = await self._setup_secrets_for_git_providers(user)
# Configure LLM and MCP
llm, mcp_config = await self._configure_llm_and_mcp(user, llm_model)

View File

@@ -9,7 +9,11 @@ from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.app_server.user.user_context import UserContext, UserContextInjector
from openhands.app_server.user.user_models import UserInfo
from openhands.integrations.provider import ProviderHandler, ProviderType
from openhands.integrations.provider import (
PROVIDER_TOKEN_TYPE,
ProviderHandler,
ProviderType,
)
from openhands.sdk.conversation.secret_source import SecretSource, StaticSecret
from openhands.server.user_auth.user_auth import UserAuth, get_user_auth
@@ -44,6 +48,9 @@ class AuthUserContext(UserContext):
self._user_info = user_info
return user_info
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
return await self.user_auth.get_provider_tokens()
async def get_provider_handler(self):
provider_handler = self._provider_handler
if not provider_handler:

View File

@@ -5,7 +5,7 @@ from fastapi import Request
from openhands.app_server.errors import OpenHandsError
from openhands.app_server.user.user_context import UserContext
from openhands.app_server.user.user_models import UserInfo
from openhands.integrations.provider import ProviderType
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.sdk.conversation.secret_source import SecretSource
@@ -24,6 +24,9 @@ class SpecifyUserContext(UserContext):
async def get_authenticated_git_url(self, repository: str) -> str:
raise NotImplementedError()
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
raise NotImplementedError()
async def get_latest_token(self, provider_type: ProviderType) -> str | None:
raise NotImplementedError()

View File

@@ -4,7 +4,7 @@ from openhands.app_server.services.injector import Injector
from openhands.app_server.user.user_models import (
UserInfo,
)
from openhands.integrations.provider import ProviderType
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.sdk.conversation.secret_source import SecretSource
from openhands.sdk.utils.models import DiscriminatedUnionMixin
@@ -26,6 +26,10 @@ class UserContext(ABC):
async def get_authenticated_git_url(self, repository: str) -> str:
"""Get the provider tokens for the user"""
@abstractmethod
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
"""Get the latest tokens for all provider types"""
@abstractmethod
async def get_latest_token(self, provider_type: ProviderType) -> str | None:
"""Get the latest token for the provider type given"""