Compare commits

...

34 Commits

Author SHA1 Message Date
Chuck Butkus
3731405e8c Fix again 2025-02-12 01:15:08 -05:00
Chuck Butkus
47ab99807b Use keycloak token in conv manager 2025-02-12 01:00:06 -05:00
Chuck Butkus
f70a9e3b93 Use keycloak token in conv manager 2025-02-12 00:47:37 -05:00
Chuck Butkus
d8d93d3616 Merge main into branch 2025-02-11 17:03:01 -05:00
Chuck Butkus
dfe8d84856 Using keycloak async methods 2025-02-08 17:07:18 -05:00
Chuck Butkus
0167c04271 Remove some logging 2025-02-08 16:20:43 -05:00
Chuck Butkus
740f17623d Add token setting on successful login 2025-02-08 02:00:35 -05:00
Chuck Butkus
383eda2032 Add keycloak client to listen.py 2025-02-07 21:36:47 -05:00
Xingyao Wang
fd30bb878a fix: set tool_choice to none for non-fncall models (#6652) 2025-02-07 17:27:53 -05:00
sp.wack
ee38994c6b chore(frontend): Take into account other error message types (#6647) 2025-02-07 17:27:53 -05:00
Xingyao Wang
0b908d3fd9 feat: Add LocalRuntime (#5284)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-02-07 17:27:53 -05:00
tofarr
30e14239c5 fix: handle SAAS mode properly in useSettings hook (#6646)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-02-07 17:27:53 -05:00
Graham Neubig
6b362b5984 Optimize memory usage in FileEditObservation (#6622)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-02-07 17:27:53 -05:00
mamoodi
07c95d2362 Add o1 to verfied models (#6642) 2025-02-07 17:27:53 -05:00
Graham Neubig
657f0b05a3 Better error logging in posthog (#6346)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-02-07 17:27:53 -05:00
sp.wack
452cd25030 chore(frontend): Migrate from NextUI to HeroUI via codemod (#6635) 2025-02-07 17:27:53 -05:00
mamoodi
df95142a89 Only show start project button in conversations (#6626)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-02-07 17:27:53 -05:00
dependabot[bot]
17670ed481 chore(deps): bump the version-all group across 1 directory with 15 updates (#6617)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-02-07 17:27:54 -05:00
Chuck Butkus
f7d07132c9 Fix listen socket code 2025-02-07 12:46:39 -05:00
Chuck Butkus
3391d8df95 Use keycloak token if available 2025-02-07 11:37:08 -05:00
Chuck Butkus
8bf899a080 Add more logging 2025-02-05 14:55:51 -05:00
chuckbutkus
e400bfaa0a Merge branch 'main' into keycloak_new 2025-02-05 14:25:31 -05:00
Chuck Butkus
7e3a259bd4 Merge branch into main 2025-02-05 14:18:13 -05:00
Chuck Butkus
96575998ec Force build 2025-02-05 13:54:19 -05:00
Chuck Butkus
295b306526 Add exception logging 2025-02-05 13:45:39 -05:00
Chuck Butkus
58f514a47e Fix syntax error 2025-02-05 12:38:38 -05:00
Chuck Butkus
d9355f19cb Fix URL again 2025-02-05 03:04:19 -05:00
Chuck Butkus
28b57b47d4 Fix OAuth URL for deployed in cluster 2025-02-05 02:27:24 -05:00
chuckbutkus
dc06a4bcdb Merge branch 'main' into keycloak_new 2025-01-31 23:09:40 -05:00
Chuck Butkus
741cf0669f Merge branch 'main' into keycloak_new 2025-01-31 22:54:01 -05:00
Chuck Butkus
1c9f0da5ea Update 2025-01-31 21:12:11 -05:00
Chuck Butkus
c4a4ddd170 Udpate 2025-01-31 14:51:55 -05:00
Chuck Butkus
5c2fd2d1ae Update 2025-01-31 02:54:08 -05:00
Chuck Butkus
929e0ec935 Initial commit 2025-01-31 02:40:37 -05:00
15 changed files with 119 additions and 35 deletions

View File

@@ -1,5 +1,6 @@
import json
import os
import subprocess
import tempfile
import time
from functools import partial
@@ -30,6 +31,7 @@ from openhands.core.config import (
SandboxConfig,
get_parser,
)
from openhands.core.config.llm_config import LLMConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
from openhands.events.action import CmdRunAction

View File

@@ -168,17 +168,25 @@ class OpenHands {
/**
* @param code Code provided by GitHub
* @param redirectUri Code provided by GitHub
* @returns GitHub access token
*/
static async getGitHubAccessToken(
code: string,
redirectUri: string,
): Promise<GitHubAccessTokenResponse> {
const { data } = await openHands.post<GitHubAccessTokenResponse>(
const { data } = await openHands.get<GitHubAccessTokenResponse>(
"/api/github/callback",
{
code,
params: {
code,
redirectUri,
},
},
);
console.debug(
`/api/github/callback response data: ${JSON.stringify(data)}`,
);
return data;
}

View File

@@ -50,5 +50,13 @@ export const useSettings = () => {
setGitHubTokenIsSet(!!query.data?.GITHUB_TOKEN_IS_SET);
}, [query.data?.GITHUB_TOKEN_IS_SET, query.isFetched]);
// Return default settings if in SAAS mode and not authenticated
if (config?.APP_MODE === "saas" && !githubTokenIsSet) {
return {
...query,
data: DEFAULT_SETTINGS,
};
}
return query;
};

View File

@@ -6,6 +6,8 @@
*/
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
const scope = "repo,user,workflow,offline_access";
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
const baseUrl = `${requestUrl.origin}`.replace("https://", "").replace("http://", "")
const scope = "openid email profile";
const force_build = true
return `https://auth.${baseUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=some-state-value&nonce=222`;
};

View File

@@ -18,18 +18,15 @@ class GitHubService:
token: SecretStr = SecretStr('')
refresh = False
def __init__(self, user_id: str | None = None, token: SecretStr | None = None):
self.user_id = user_id
def __init__(self, user_key: str | None, token: SecretStr | None = None):
self.user_key = user_key
if token:
self.token = token
async def _get_github_headers(self) -> dict:
"""
Retrieve the GH Token from settings store to construct the headers
"""
if self.user_id and not self.token:
"""Retrieve the GH Token from settings store to construct the headers."""
if self.user_key and not self.token:
self.token = await self.get_latest_token()
return {

View File

@@ -219,7 +219,7 @@ class Runtime(FileEditRuntimeMixin):
try:
if isinstance(event, CmdRunAction):
if self.github_user_id and '$GITHUB_TOKEN' in event.command:
gh_client = GithubServiceImpl(user_id=self.github_user_id)
gh_client = GithubServiceImpl(user_key=self.github_user_id)
token = await gh_client.get_latest_token()
if token:
export_cmd = CmdRunAction(

View File

@@ -405,11 +405,11 @@ class DockerRuntime(ActionExecutionClient):
"""Pause the runtime by stopping the container.
This is different from container.stop() as it ensures environment variables are properly preserved."""
if not self.container:
raise RuntimeError("Container not initialized")
raise RuntimeError('Container not initialized')
# First, ensure all environment variables are properly persisted in .bashrc
# This is already handled by add_env_vars in base.py
# Stop the container
self.container.stop()
self.log('debug', f'Container {self.container_name} paused')
@@ -418,12 +418,12 @@ class DockerRuntime(ActionExecutionClient):
"""Resume the runtime by starting the container.
This is different from container.start() as it ensures environment variables are properly restored."""
if not self.container:
raise RuntimeError("Container not initialized")
raise RuntimeError('Container not initialized')
# Start the container
self.container.start()
self.log('debug', f'Container {self.container_name} resumed')
# Wait for the container to be ready
self._wait_until_alive()

View File

@@ -13,4 +13,3 @@ function deactivate() {}
module.exports = {
activate,
deactivate
}

View File

@@ -20,4 +20,4 @@
"title": "Hello World from OpenHands"
}]
}
}
}

View File

@@ -8,3 +8,7 @@ def get_github_token(request: Request) -> SecretStr | None:
def get_user_id(request: Request) -> str | None:
return getattr(request.state, 'github_user_id', None)
def get_keycloak_token(request: Request) -> str | None:
return getattr(request.state, 'keycloak_token', None)

View File

@@ -1,6 +1,8 @@
import os
from urllib.parse import parse_qs
import jwt
from keycloak import KeycloakOpenID
from pydantic import SecretStr
from socketio.exceptions import ConnectionRefusedError
@@ -52,7 +54,20 @@ async def connect(connection_id: str, environ, auth):
else config.jwt_secret
)
decoded = jwt.decode(signed_token, jwt_secret, algorithms=['HS256'])
user_id = decoded['github_user_id']
access_token = decoded['access_token']
KEYCLOAK_SERVER_URL = os.getenv('KEYCLOAK_SERVER_URL', '')
KEYCLOAK_REALM_NAME = os.getenv('KEYCLOAK_REALM_NAME', '')
KEYCLOAK_CLIENT_ID = os.getenv('KEYCLOAK_CLIENT_ID', '')
KEYCLOAK_CLIENT_SECRET = os.getenv('KEYCLOAK_CLIENT_SECRET', '')
keycloak_openid = KeycloakOpenID(
server_url=KEYCLOAK_SERVER_URL,
realm_name=KEYCLOAK_REALM_NAME,
client_id=KEYCLOAK_CLIENT_ID,
client_secret_key=KEYCLOAK_CLIENT_SECRET,
)
user_info = await keycloak_openid.a_userinfo(access_token)
user_id = user_info['github_id']
logger.info(f'User {user_id} is connecting to conversation {conversation_id}')

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.github.github_types import (
GhAuthenticationError,
@@ -9,7 +9,7 @@ from openhands.integrations.github.github_types import (
GitHubRepository,
GitHubUser,
)
from openhands.server.auth import get_github_token, get_user_id
from openhands.server.auth import get_keycloak_token, get_user_id
app = APIRouter(prefix='/api/github')
@@ -21,9 +21,13 @@ async def get_github_repositories(
sort: str = 'pushed',
installation_id: int | None = None,
github_user_id: str | None = Depends(get_user_id),
github_user_token: SecretStr | None = Depends(get_github_token),
keycloak_token: str | None = Depends(get_keycloak_token),
):
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
client = (
GithubServiceImpl(keycloak_token)
if keycloak_token
else GithubServiceImpl(github_user_id)
)
try:
repos: list[GitHubRepository] = await client.get_repositories(
page, per_page, sort, installation_id
@@ -31,62 +35,94 @@ async def get_github_repositories(
return repos
except GhAuthenticationError as e:
logger.error("Couldn't search GitHub repositories")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=401,
)
except GHUnknownException as e:
logger.error("Couldn't search GitHub repositories")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=500,
)
except Exception as e:
logger.error("Couldn't get GitHub repositories")
logger.error(e)
raise
@app.get('/user')
async def get_github_user(
github_user_id: str | None = Depends(get_user_id),
github_user_token: SecretStr | None = Depends(get_github_token),
keycloak_token: str | None = Depends(get_keycloak_token),
):
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
client = (
GithubServiceImpl(keycloak_token)
if keycloak_token
else GithubServiceImpl(github_user_id)
)
try:
user: GitHubUser = await client.get_user()
return user
except GhAuthenticationError as e:
logger.error("Couldn't get GitHub user")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=401,
)
except GHUnknownException as e:
logger.error("Couldn't get GitHub user")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=500,
)
except Exception as e:
logger.error("Couldn't get GitHub repositories")
logger.error(e)
raise
@app.get('/installations')
async def get_github_installation_ids(
github_user_id: str | None = Depends(get_user_id),
github_user_token: SecretStr | None = Depends(get_github_token),
keycloak_token: str | None = Depends(get_keycloak_token),
):
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
client = (
GithubServiceImpl(keycloak_token)
if keycloak_token
else GithubServiceImpl(github_user_id)
)
try:
installations_ids: list[int] = await client.get_installation_ids()
return installations_ids
except GhAuthenticationError as e:
logger.error("Couldn't get GitHub installations")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=401,
)
except GHUnknownException as e:
logger.error("Couldn't get GitHub installations")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=500,
)
except Exception as e:
logger.error("Couldn't get GitHub repositories")
logger.error(e)
raise
@app.get('/search/repositories')
@@ -96,9 +132,13 @@ async def search_github_repositories(
sort: str = 'stars',
order: str = 'desc',
github_user_id: str | None = Depends(get_user_id),
github_user_token: SecretStr | None = Depends(get_github_token),
keycloak_token: str | None = Depends(get_keycloak_token),
):
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
client = (
GithubServiceImpl(keycloak_token)
if keycloak_token
else GithubServiceImpl(github_user_id)
)
try:
repos: list[GitHubRepository] = await client.search_repositories(
query, per_page, sort, order
@@ -106,13 +146,21 @@ async def search_github_repositories(
return repos
except GhAuthenticationError as e:
logger.error("Couldn't search GitHub repositories")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=401,
)
except GHUnknownException as e:
logger.error("Couldn't search GitHub repositories")
logger.error(e)
return JSONResponse(
content=str(e),
status_code=500,
)
except Exception as e:
logger.error("Couldn't get GitHub repositories")
logger.error(e)
raise

View File

@@ -11,7 +11,7 @@ from openhands.events.action.message import MessageAction
from openhands.events.stream import EventStreamSubscriber
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.runtime import get_runtime_cls
from openhands.server.auth import get_github_token, get_user_id
from openhands.server.auth import get_keycloak_token, get_user_id
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import (
ConversationStoreImpl,
@@ -131,7 +131,8 @@ async def new_conversation(request: Request, data: InitSessionRequest):
"""
logger.info('Initializing new conversation')
user_id = get_user_id(request)
gh_client = GithubServiceImpl(user_id=user_id, token=get_github_token(request))
access_token = get_keycloak_token(request)
gh_client = GithubServiceImpl(user_key=access_token)
github_token = await gh_client.get_latest_token()
selected_repository = data.selected_repository

View File

@@ -52,7 +52,7 @@ async def store_settings(
# We check if the token is valid by getting the user
# If the token is invalid, this will raise an exception
github = GithubServiceImpl(
user_id=None, token=SecretStr(settings.github_token)
user_key=None, token=SecretStr(settings.github_token)
)
await github.get_user()

View File

@@ -35,7 +35,7 @@ class Settings(BaseModel):
if context and context.get('expose_secrets', False):
return llm_api_key.get_secret_value()
return pydantic_encoder(llm_api_key)
return pydantic_encoder(llm_api_key) if llm_api_key is not None else None
@field_serializer('github_token')
def github_token_serializer(