(Hotfix): tokens go stale for restarted convos in cloud openhands (#9111)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra 2025-06-20 12:16:42 -04:00 committed by GitHub
parent 075ef4db9f
commit ee64a6662a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 100 additions and 60 deletions

View File

@ -293,9 +293,11 @@ class OpenHands {
static async startConversation(
conversationId: string,
providers?: Provider[],
): Promise<Conversation | null> {
const { data } = await openHands.post<Conversation | null>(
`/api/conversations/${conversationId}/start`,
providers ? { providers_set: providers } : {},
);
return data;

View File

@ -348,6 +348,7 @@ export function WsClientProvider({
conversation?.url,
conversation?.status,
conversation?.runtime_status,
providers,
]);
React.useEffect(

View File

@ -1,9 +1,16 @@
import React from "react";
import { convertRawProvidersToList } from "#/utils/convert-raw-providers-to-list";
import { useSettings } from "./query/use-settings";
export const useUserProviders = () => {
const { data: settings } = useSettings();
const providers = React.useMemo(
() => convertRawProvidersToList(settings?.PROVIDER_TOKENS_SET),
[settings?.PROVIDER_TOKENS_SET],
);
return {
providers: convertRawProvidersToList(settings?.PROVIDER_TOKENS_SET),
providers,
};
};

View File

@ -37,6 +37,7 @@ import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { TabContent } from "#/components/layout/tab-content";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
import { useUserProviders } from "#/hooks/use-user-providers";
function AppContent() {
useConversationConfig();
@ -45,6 +46,7 @@ function AppContent() {
const { conversationId } = useConversationId();
const { data: conversation, isFetched, refetch } = useActiveConversation();
const { data: isAuthed } = useIsAuthed();
const { providers } = useUserProviders();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
@ -63,11 +65,11 @@ function AppContent() {
navigate("/");
} else if (conversation?.status === "STOPPED") {
// start the conversation if the state is stopped on initial load
OpenHands.startConversation(conversation.conversation_id).then(() =>
refetch(),
OpenHands.startConversation(conversation.conversation_id, providers).then(
() => refetch(),
);
}
}, [conversation?.conversation_id, isFetched, isAuthed]);
}, [conversation?.conversation_id, isFetched, isAuthed, providers]);
React.useEffect(() => {
dispatch(clearTerminal());

View File

@ -1,6 +1,5 @@
import asyncio
import os
from types import MappingProxyType
from typing import Any
from urllib.parse import parse_qs
@ -20,66 +19,17 @@ from openhands.events.observation.agent import (
AgentStateChangedObservation,
)
from openhands.events.serialization import event_to_dict
from openhands.experiments.experiment_manager import ExperimentManagerImpl
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderToken
from openhands.integrations.service_types import ProviderType
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.services.conversation_service import (
setup_init_convo_settings,
)
from openhands.server.shared import (
SecretsStoreImpl,
SettingsStoreImpl,
config,
conversation_manager,
server_config,
sio,
)
from openhands.server.types import AppMode
from openhands.storage.conversation.conversation_validator import (
create_conversation_validator,
)
from openhands.storage.data_models.user_secrets import UserSecrets
def create_provider_tokens_object(
providers_set: list[ProviderType],
) -> PROVIDER_TOKEN_TYPE:
provider_information: dict[ProviderType, ProviderToken] = {}
for provider in providers_set:
provider_information[provider] = ProviderToken(token=None, user_id=None)
return MappingProxyType(provider_information)
async def setup_init_convo_settings(
user_id: str | None, conversation_id: str, providers_set: list[ProviderType]
) -> ConversationInitData:
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
user_secrets: UserSecrets | None = await secrets_store.load()
if not settings:
raise ConnectionRefusedError(
'Settings not found', {'msg_id': 'CONFIGURATION$SETTINGS_NOT_FOUND'}
)
session_init_args: dict = {}
session_init_args = {**settings.__dict__, **session_init_args}
git_provider_tokens = create_provider_tokens_object(providers_set)
if server_config.app_mode != AppMode.SAAS and user_secrets:
git_provider_tokens = user_secrets.provider_tokens
session_init_args['git_provider_tokens'] = git_provider_tokens
if user_secrets:
session_init_args['custom_secrets'] = user_secrets.custom_secrets
convo_init_data = ConversationInitData(**session_init_args)
# We should recreate the same experiment conditions when restarting a conversation
return ExperimentManagerImpl.run_conversation_variant_test(
user_id, conversation_id, convo_init_data
)
@sio.event
@ -170,6 +120,7 @@ async def connect(connection_id: str, environ: dict) -> None:
conversation_init_data = await setup_init_convo_settings(
user_id, conversation_id, providers_set
)
agent_loop_info = await conversation_manager.join_conversation(
conversation_id,
connection_id,

View File

@ -38,7 +38,10 @@ from openhands.server.data_models.conversation_info_result_set import (
ConversationInfoResultSet,
)
from openhands.server.dependencies import get_dependencies
from openhands.server.services.conversation_service import create_new_conversation
from openhands.server.services.conversation_service import (
create_new_conversation,
setup_init_convo_settings,
)
from openhands.server.session.conversation import ServerConversation
from openhands.server.shared import (
ConversationStoreImpl,
@ -95,6 +98,10 @@ class ConversationResponse(BaseModel):
conversation_status: ConversationStatus | None = None
class ProvidersSetModel(BaseModel):
providers_set: list[ProviderType] | None = None
@app.post('/conversations')
async def new_conversation(
data: InitSessionRequest,
@ -395,6 +402,7 @@ async def _get_conversation_info(
@app.post('/conversations/{conversation_id}/start')
async def start_conversation(
conversation_id: str,
providers_set: ProvidersSetModel,
user_id: str = Depends(get_user_id),
settings: Settings = Depends(get_user_settings),
conversation_store: ConversationStore = Depends(get_conversation_store),
@ -420,10 +428,15 @@ async def start_conversation(
status_code=status.HTTP_404_NOT_FOUND,
)
# Set up conversation init data with provider information
conversation_init_data = await setup_init_convo_settings(
user_id, conversation_id, providers_set.providers_set or []
)
# Start the agent loop
agent_loop_info = await conversation_manager.maybe_start_agent_loop(
sid=conversation_id,
settings=settings,
settings=conversation_init_data,
user_id=user_id,
)

View File

@ -1,4 +1,5 @@
import uuid
from types import MappingProxyType
from typing import Any
from openhands.core.logger import openhands_logger as logger
@ -7,21 +8,25 @@ from openhands.experiments.experiment_manager import ExperimentManagerImpl
from openhands.integrations.provider import (
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA,
PROVIDER_TOKEN_TYPE,
ProviderToken,
)
from openhands.integrations.service_types import ProviderType
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import (
ConversationStoreImpl,
SecretsStoreImpl,
SettingsStoreImpl,
config,
conversation_manager,
server_config,
)
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.server.types import AppMode, LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.utils.conversation_summary import get_default_conversation_title
@ -135,3 +140,62 @@ async def create_new_conversation(
)
logger.info(f'Finished initializing conversation {agent_loop_info.conversation_id}')
return agent_loop_info
def create_provider_tokens_object(
providers_set: list[ProviderType],
) -> PROVIDER_TOKEN_TYPE:
"""Create provider tokens object for the given providers."""
provider_information: dict[ProviderType, ProviderToken] = {}
for provider in providers_set:
provider_information[provider] = ProviderToken(token=None, user_id=None)
return MappingProxyType(provider_information)
async def setup_init_convo_settings(
user_id: str | None, conversation_id: str, providers_set: list[ProviderType]
) -> ConversationInitData:
"""Set up conversation initialization data with provider tokens.
Args:
user_id: The user ID
conversation_id: The conversation ID
providers_set: List of provider types to set up tokens for
Returns:
ConversationInitData with provider tokens configured
"""
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
user_secrets: UserSecrets | None = await secrets_store.load()
if not settings:
from socketio.exceptions import ConnectionRefusedError
raise ConnectionRefusedError(
'Settings not found', {'msg_id': 'CONFIGURATION$SETTINGS_NOT_FOUND'}
)
session_init_args: dict = {}
session_init_args = {**settings.__dict__, **session_init_args}
git_provider_tokens = create_provider_tokens_object(providers_set)
logger.info(f'Git provider scaffold: {git_provider_tokens}')
if server_config.app_mode != AppMode.SAAS and user_secrets:
git_provider_tokens = user_secrets.provider_tokens
session_init_args['git_provider_tokens'] = git_provider_tokens
if user_secrets:
session_init_args['custom_secrets'] = user_secrets.custom_secrets
convo_init_data = ConversationInitData(**session_init_args)
# We should recreate the same experiment conditions when restarting a conversation
return ExperimentManagerImpl.run_conversation_variant_test(
user_id, conversation_id, convo_init_data
)