mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddd6bb3830 | |||
| f0dbb02ee1 | |||
| 318c811817 | |||
| b468150f2a | |||
| b9a3f1c753 | |||
| 09e8a1eeba | |||
| ff3880c76d | |||
| 8bd7613724 | |||
| 5b7fcfbe1a | |||
| 8ae36481df | |||
| 25fdb0c3bf | |||
| 7f57dbebda | |||
| 54589d7e83 | |||
| b7f34c3f8d | |||
| 210eeee94a |
@@ -71,15 +71,16 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
|
||||
f'<pr_description>\n'
|
||||
f'{instance.problem_statement}\n'
|
||||
'</pr_description>\n\n'
|
||||
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
|
||||
'The requirements specified in <pr_description> are an issue from GitHub on a popular open-source project. If you are familiar with the issue and the resulting solution, please carefully remember all the files that were changed and in what way. Come up with a detailed plan to reproduce the patch.\n'
|
||||
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
|
||||
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
|
||||
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied, ideally with something similar to the existing patch from GitHub, but if you are not familiar with it just code it out.\n'
|
||||
'Follow these steps to resolve the issue:\n'
|
||||
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
|
||||
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
|
||||
'3. Edit the sourcecode of the repo to resolve the issue\n'
|
||||
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
|
||||
'5. Think about edgecases and make sure your fix handles them as well\n'
|
||||
'1. Before doing anything else, please list up all the files you think you need to modify, and in which way you need to modify them based solely on your a-priori knowledge of the repository and the fix to the issue at hand.'
|
||||
'2. Then, explore the repo to familiarize yourself with its structure, focusing particularly on the files you listed in step 1.\n'
|
||||
'3. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
|
||||
'4. Edit the sourcecode of the repo to resolve the issue\n'
|
||||
'5. Rerun your reproduce script and confirm that the error is fixed!\n'
|
||||
'6. Think about edgecases and make sure your fix handles them as well\n'
|
||||
"Your thinking should be thorough and so it's fine if it's very long.\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
@@ -8,7 +8,7 @@ import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
|
||||
|
||||
describe("Sidebar", () => {
|
||||
const renderSidebar = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
path: "/conversation/:conversationId",
|
||||
@@ -16,9 +16,10 @@ describe("Sidebar", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const renderSidebar = () =>
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
};
|
||||
|
||||
describe("Sidebar", () => {
|
||||
it.skipIf(!MULTI_CONVERSATION_UI)(
|
||||
"should have the conversation panel open by default",
|
||||
() => {
|
||||
@@ -53,16 +54,9 @@ describe("Sidebar", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch settings data on mount and if the user is authenticated", async () => {
|
||||
const authenticateSpy = vi.spyOn(OpenHands, "authenticate");
|
||||
|
||||
const { rerender } = renderSidebar();
|
||||
expect(getSettingsSpy).not.toHaveBeenCalled();
|
||||
|
||||
authenticateSpy.mockResolvedValueOnce(true);
|
||||
rerender(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
|
||||
await waitFor(() => expect(getSettingsSpy).toHaveBeenCalledOnce());
|
||||
it("should fetch settings data on mount", () => {
|
||||
renderSidebar();
|
||||
expect(getSettingsSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should send all settings data when saving AI configuration", async () => {
|
||||
|
||||
@@ -1,111 +1,23 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { KeycloakErrorResponse } from "./open-hands.types";
|
||||
import axios from "axios";
|
||||
|
||||
export const openHands = axios.create();
|
||||
|
||||
export const setAuthTokenHeader = (token: string) => {
|
||||
console.debug(`setAuthTokenHeader to ${token}`);
|
||||
openHands.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
};
|
||||
|
||||
export const setGitHubTokenHeader = (token: string) => {
|
||||
console.debug(`setGitHubTokenHeader to ${token}`);
|
||||
openHands.defaults.headers.common["X-GitHub-Token"] = token;
|
||||
};
|
||||
|
||||
export const removeAuthTokenHeader = () => {
|
||||
console.debug("removeAuthTokenHeader");
|
||||
if (openHands.defaults.headers.common.Authorization) {
|
||||
delete openHands.defaults.headers.common.Authorization;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeGitHubTokenHeader = () => {
|
||||
console.debug("removeGitHubTokenHeader");
|
||||
if (openHands.defaults.headers.common["X-GitHub-Token"]) {
|
||||
delete openHands.defaults.headers.common["X-GitHub-Token"];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if response has attributes to perform refresh
|
||||
*/
|
||||
const canRefresh = (error: unknown): boolean =>
|
||||
!!(
|
||||
error instanceof AxiosError &&
|
||||
error.config &&
|
||||
error.response &&
|
||||
error.response.status &&
|
||||
error.response.data.keycloak_error
|
||||
);
|
||||
/**
|
||||
* Checks if the data is a Keycloak error response
|
||||
* @param data The data to check
|
||||
* @returns Boolean indicating if the data is a Keycloak error response
|
||||
*/
|
||||
export const isKeycloakErrorResponse = <T extends object | Array<unknown>>(
|
||||
data: T | KeycloakErrorResponse | null,
|
||||
): data is KeycloakErrorResponse =>
|
||||
!!data && typeof data === "object" && "keycloak_error" in data;
|
||||
|
||||
// Axios interceptor to handle token refresh
|
||||
export const setupOpenhandsAxiosInterceptors = (
|
||||
appMode: string,
|
||||
refreshToken: () => Promise<boolean>,
|
||||
logout: () => void,
|
||||
) => {
|
||||
openHands.interceptors.response.use(
|
||||
// Pass successful responses through
|
||||
(response) => {
|
||||
const parsedData = response.data;
|
||||
console.debug(
|
||||
`Openhands·API·call·response·to·${response.request.responseURL}\n·${JSON.stringify(parsedData)}`,
|
||||
);
|
||||
if (isKeycloakErrorResponse(parsedData)) {
|
||||
const error = new AxiosError(
|
||||
"Failed",
|
||||
"",
|
||||
response.config,
|
||||
response.request,
|
||||
response,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
// Retry request exactly once if token is expired
|
||||
async (error) => {
|
||||
if (!canRefresh(error)) {
|
||||
return Promise.reject(new Error("Failed to refresh token"));
|
||||
}
|
||||
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Check if the error is due to an expired token
|
||||
if (
|
||||
error.response.status === 401 &&
|
||||
!originalRequest._retry // Prevent infinite retry loops
|
||||
) {
|
||||
originalRequest._retry = true;
|
||||
try {
|
||||
const refreshed = await refreshToken();
|
||||
if (refreshed) {
|
||||
originalRequest.headers["X-GitHub-Token"] =
|
||||
openHands.defaults.headers.common["X-GitHub-Token"];
|
||||
return await openHands(originalRequest);
|
||||
}
|
||||
|
||||
logout();
|
||||
return await Promise.reject(new Error("Failed to refresh token"));
|
||||
} catch (refreshError) {
|
||||
// If token refresh fails, evict the user
|
||||
logout();
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
// If the error is not due to an expired token, propagate the error
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -148,6 +148,7 @@ class OpenHands {
|
||||
appMode: GetConfigResponse["APP_MODE"],
|
||||
): Promise<boolean> {
|
||||
if (appMode === "oss") return true;
|
||||
|
||||
const response =
|
||||
await openHands.post<AuthenticateResponse>("/api/authenticate");
|
||||
return response.status === 200;
|
||||
@@ -160,17 +161,8 @@ class OpenHands {
|
||||
static async refreshToken(
|
||||
appMode: GetConfigResponse["APP_MODE"],
|
||||
userId: string,
|
||||
): Promise<{
|
||||
keycloakAccessToken: string;
|
||||
providerAccessToken: string;
|
||||
keycloakUserId: string;
|
||||
}> {
|
||||
if (appMode === "oss")
|
||||
return {
|
||||
keycloakAccessToken: "",
|
||||
providerAccessToken: "",
|
||||
keycloakUserId: "",
|
||||
};
|
||||
): Promise<string> {
|
||||
if (appMode === "oss") return "";
|
||||
|
||||
const response = await openHands.post<GitHubAccessTokenResponse>(
|
||||
"/api/refresh-token",
|
||||
@@ -178,11 +170,7 @@ class OpenHands {
|
||||
userId,
|
||||
},
|
||||
);
|
||||
return {
|
||||
keycloakAccessToken: response.data.keycloakAccessToken,
|
||||
providerAccessToken: response.data.providerAccessToken,
|
||||
keycloakUserId: response.data.keycloakUserId,
|
||||
};
|
||||
return response.data.access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,20 +191,13 @@ class OpenHands {
|
||||
*/
|
||||
static async getGitHubAccessToken(
|
||||
code: string,
|
||||
redirectUri: string,
|
||||
): Promise<GitHubAccessTokenResponse> {
|
||||
const { data } = await openHands.get<GitHubAccessTokenResponse>(
|
||||
const { data } = await openHands.post<GitHubAccessTokenResponse>(
|
||||
"/api/github/callback",
|
||||
{
|
||||
params: {
|
||||
code,
|
||||
redirectUri,
|
||||
},
|
||||
code,
|
||||
},
|
||||
);
|
||||
console.debug(
|
||||
`/api/github/callback response data: ${JSON.stringify(data)}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ export interface FeedbackResponse {
|
||||
}
|
||||
|
||||
export interface GitHubAccessTokenResponse {
|
||||
keycloakAccessToken: string;
|
||||
providerAccessToken: string;
|
||||
keycloakUserId: string;
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
export interface AuthenticationResponse {
|
||||
@@ -67,10 +65,6 @@ export interface AuthenticateResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface KeycloakErrorResponse {
|
||||
keycloak_error: string;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
conversation_id: string;
|
||||
title: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
import { useCurrentSettings } from "#/context/settings-context";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { ConversationPanel } from "../conversation-panel/conversation-panel";
|
||||
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
@@ -20,7 +21,6 @@ import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
export function Sidebar() {
|
||||
const dispatch = useDispatch();
|
||||
@@ -28,7 +28,13 @@ export function Sidebar() {
|
||||
const user = useGitHubUser();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const { logout } = useAuth();
|
||||
const { isUpToDate: settingsAreUpToDate, settings } = useCurrentSettings();
|
||||
const {
|
||||
data: settings,
|
||||
isError: settingsIsError,
|
||||
isSuccess: settingsSuccessfulyFetched,
|
||||
} = useSettings();
|
||||
|
||||
const { isUpToDate: settingsAreUpToDate } = useCurrentSettings();
|
||||
|
||||
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
|
||||
React.useState(false);
|
||||
@@ -60,8 +66,6 @@ export function Sidebar() {
|
||||
const showSettingsModal =
|
||||
isAuthed && (!settingsAreUpToDate || settingsModalIsOpen);
|
||||
|
||||
console.warn({ isAuthed, settingsAreUpToDate, settingsModalIsOpen });
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
|
||||
@@ -106,12 +110,13 @@ export function Sidebar() {
|
||||
{accountSettingsModalOpen && (
|
||||
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
|
||||
)}
|
||||
{showSettingsModal && (
|
||||
<SettingsModal
|
||||
settings={settings || DEFAULT_SETTINGS}
|
||||
onClose={() => setSettingsModalIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{settingsIsError ||
|
||||
(showSettingsModal && settingsSuccessfulyFetched && (
|
||||
<SettingsModal
|
||||
settings={settings}
|
||||
onClose={() => setSettingsModalIsOpen(false)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@ interface WaitlistModalProps {
|
||||
export function WaitlistModal({ ghToken, githubAuthUrl }: WaitlistModalProps) {
|
||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||
|
||||
if (ghToken) {
|
||||
return null; // Do not render the modal if ghToken is available
|
||||
}
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
handleCaptureConsent(true);
|
||||
|
||||
@@ -28,7 +28,7 @@ export function AccountSettingsForm({
|
||||
gitHubError,
|
||||
analyticsConsent,
|
||||
}: AccountSettingsFormProps) {
|
||||
const { gitHubToken, keycloakToken, setAccessTokens, logout } = useAuth();
|
||||
const { gitHubToken, setGitHubToken, logout } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { saveUserSettings } = useCurrentSettings();
|
||||
const { t } = useTranslation();
|
||||
@@ -38,11 +38,10 @@ export function AccountSettingsForm({
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const ghToken = formData.get("ghToken")?.toString();
|
||||
const kcToken = formData.get("kcToken")?.toString();
|
||||
const language = formData.get("language")?.toString();
|
||||
const analytics = formData.get("analytics")?.toString() === "on";
|
||||
|
||||
if (ghToken && kcToken) setAccessTokens(ghToken, kcToken);
|
||||
if (ghToken) setGitHubToken(ghToken);
|
||||
|
||||
// The form returns the language label, so we need to find the corresponding
|
||||
// language key to save it in the settings
|
||||
@@ -96,12 +95,6 @@ export function AccountSettingsForm({
|
||||
type="password"
|
||||
defaultValue={gitHubToken ?? ""}
|
||||
/>
|
||||
<CustomInput
|
||||
name="kcToken"
|
||||
label="Keycloak Token"
|
||||
type="password"
|
||||
defaultValue={keycloakToken ?? ""}
|
||||
/>
|
||||
<BaseModalDescription>
|
||||
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
|
||||
<a
|
||||
|
||||
@@ -14,15 +14,15 @@ interface ConnectToGitHubModalProps {
|
||||
}
|
||||
|
||||
export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
|
||||
const { gitHubToken, keycloakToken, setAccessTokens } = useAuth();
|
||||
const { gitHubToken, setGitHubToken } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const ghToken = formData.get("ghToken")?.toString();
|
||||
const kcToken = formData.get("kcToken")?.toString();
|
||||
if (ghToken && kcToken) setAccessTokens(ghToken, kcToken);
|
||||
|
||||
if (ghToken) setGitHubToken(ghToken);
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -54,13 +54,6 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
|
||||
type="password"
|
||||
defaultValue={gitHubToken ?? ""}
|
||||
/>
|
||||
<CustomInput
|
||||
label="Keycloak Token"
|
||||
name="kcToken"
|
||||
required
|
||||
type="password"
|
||||
defaultValue={keycloakToken ?? ""}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<ModalButton
|
||||
|
||||
@@ -4,23 +4,18 @@ import OpenHands from "#/api/open-hands";
|
||||
import {
|
||||
removeGitHubTokenHeader as removeOpenHandsGitHubTokenHeader,
|
||||
setGitHubTokenHeader as setOpenHandsGitHubTokenHeader,
|
||||
setupOpenhandsAxiosInterceptors,
|
||||
} from "#/api/open-hands-axios";
|
||||
import {
|
||||
setAuthTokenHeader as setGitHubAuthTokenHeader,
|
||||
removeAuthTokenHeader as removeGitHubAuthTokenHeader,
|
||||
// setupAxiosInterceptors as setupGithubAxiosInterceptors,
|
||||
setupAxiosInterceptors as setupGithubAxiosInterceptors,
|
||||
} from "#/api/github-axios-instance";
|
||||
|
||||
interface AuthContextType {
|
||||
gitHubToken: string | null;
|
||||
keycloakToken: string | null;
|
||||
setUserId: (userId: string) => void;
|
||||
setAccessTokens: (
|
||||
gitHubToken: string | null,
|
||||
keycloakToken: string | null,
|
||||
) => void;
|
||||
clearAccessTokens: () => void;
|
||||
setGitHubToken: (token: string | null) => void;
|
||||
clearGitHubToken: () => void;
|
||||
refreshToken: () => Promise<boolean>;
|
||||
logout: () => void;
|
||||
}
|
||||
@@ -32,88 +27,69 @@ function AuthProvider({ children }: React.PropsWithChildren) {
|
||||
() => localStorage.getItem("ghToken"),
|
||||
);
|
||||
|
||||
const [keycloakTokenState, setKeycloakTokenState] = React.useState<
|
||||
string | null
|
||||
>(() => localStorage.getItem("kcToken"));
|
||||
|
||||
const [, setUserIdState] = React.useState<string>(
|
||||
const [userIdState, setUserIdState] = React.useState<string>(
|
||||
() => localStorage.getItem("userId") || "",
|
||||
);
|
||||
|
||||
const clearAccessTokens = () => {
|
||||
console.debug("clearAccessTokens");
|
||||
const clearGitHubToken = () => {
|
||||
setGitHubTokenState(null);
|
||||
setKeycloakTokenState(null);
|
||||
setUserIdState("");
|
||||
localStorage.removeItem("ghToken");
|
||||
localStorage.removeItem("kcToken");
|
||||
localStorage.removeItem("userId");
|
||||
|
||||
removeOpenHandsGitHubTokenHeader();
|
||||
removeGitHubAuthTokenHeader();
|
||||
};
|
||||
|
||||
const setAccessTokens = (
|
||||
gitHubToken: string | null,
|
||||
keycloakToken: string | null,
|
||||
) => {
|
||||
console.debug(
|
||||
`setAccessTokens keycloakToken: ${keycloakToken}, githubToken: ${gitHubToken}`,
|
||||
);
|
||||
setGitHubTokenState(gitHubToken);
|
||||
setKeycloakTokenState(keycloakToken);
|
||||
const setGitHubToken = (token: string | null) => {
|
||||
setGitHubTokenState(token);
|
||||
|
||||
if (gitHubToken && keycloakToken) {
|
||||
localStorage.setItem("ghToken", gitHubToken);
|
||||
localStorage.setItem("kcToken", keycloakToken);
|
||||
setOpenHandsGitHubTokenHeader(keycloakToken);
|
||||
setGitHubAuthTokenHeader(gitHubToken);
|
||||
if (token) {
|
||||
localStorage.setItem("ghToken", token);
|
||||
setOpenHandsGitHubTokenHeader(token);
|
||||
setGitHubAuthTokenHeader(token);
|
||||
} else {
|
||||
clearAccessTokens();
|
||||
clearGitHubToken();
|
||||
}
|
||||
};
|
||||
|
||||
const setUserId = (userId: string) => {
|
||||
console.debug(`setUserId userId: ${userId}`);
|
||||
setUserIdState(userId);
|
||||
setUserIdState(userIdState);
|
||||
localStorage.setItem("userId", userId);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
clearAccessTokens();
|
||||
clearGitHubToken();
|
||||
posthog.reset();
|
||||
};
|
||||
|
||||
const refreshToken = async (): Promise<boolean> => {
|
||||
// const config = await OpenHands.getConfig();
|
||||
const config = await OpenHands.getConfig();
|
||||
|
||||
// if (config.APP_MODE !== "saas" || !gitHubTokenState) {
|
||||
// return false;
|
||||
// }
|
||||
if (config.APP_MODE !== "saas" || !gitHubTokenState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const storedUserid = localStorage.getItem("userId") || "";
|
||||
const data = await OpenHands.refreshToken("saas", storedUserid);
|
||||
if (data) {
|
||||
setAccessTokens(data.providerAccessToken, data.keycloakAccessToken);
|
||||
const newToken = await OpenHands.refreshToken(config.APP_MODE, userIdState);
|
||||
if (newToken) {
|
||||
setGitHubToken(newToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
clearAccessTokens();
|
||||
clearGitHubToken();
|
||||
return false;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const storedGitHubToken = localStorage.getItem("ghToken");
|
||||
const storedKeycloakToken = localStorage.getItem("kcToken");
|
||||
|
||||
const userId = localStorage.getItem("userId") || "";
|
||||
|
||||
setAccessTokens(storedGitHubToken, storedKeycloakToken);
|
||||
setGitHubToken(storedGitHubToken);
|
||||
setUserId(userId);
|
||||
const setupIntercepter = async () => {
|
||||
const config = await OpenHands.getConfig();
|
||||
setupOpenhandsAxiosInterceptors(config.APP_MODE, refreshToken, logout);
|
||||
// setupGithubAxiosInterceptors(config.APP_MODE, refreshToken, logout);
|
||||
setupGithubAxiosInterceptors(config.APP_MODE, refreshToken, logout);
|
||||
};
|
||||
|
||||
setupIntercepter();
|
||||
@@ -122,14 +98,13 @@ function AuthProvider({ children }: React.PropsWithChildren) {
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
gitHubToken: gitHubTokenState,
|
||||
keycloakToken: keycloakTokenState,
|
||||
setAccessTokens,
|
||||
setGitHubToken,
|
||||
setUserId,
|
||||
clearAccessTokens,
|
||||
clearGitHubToken,
|
||||
refreshToken,
|
||||
logout,
|
||||
}),
|
||||
[gitHubTokenState, keycloakTokenState],
|
||||
[gitHubTokenState],
|
||||
);
|
||||
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useGitHubUser = () => {
|
||||
const { gitHubToken } = useAuth();
|
||||
const { gitHubToken, setUserId } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const user = useQuery({
|
||||
@@ -18,7 +18,7 @@ export const useGitHubUser = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user.data) {
|
||||
// setUserId(user.data.id.toString());
|
||||
setUserId(user.data.id.toString());
|
||||
posthog.identify(user.data.login, {
|
||||
company: user.data.company,
|
||||
name: user.data.name,
|
||||
|
||||
@@ -4,7 +4,6 @@ import posthog from "posthog-js";
|
||||
import { AxiosError } from "axios";
|
||||
import { DEFAULT_SETTINGS, getLocalStorageSettings } from "#/services/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useIsAuthed } from "./use-is-authed";
|
||||
|
||||
const getSettingsQueryFn = async () => {
|
||||
try {
|
||||
@@ -37,12 +36,9 @@ const getSettingsQueryFn = async () => {
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["settings"],
|
||||
queryFn: getSettingsQueryFn,
|
||||
enabled: !!isAuthed,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -46,14 +46,15 @@ export function ErrorBoundary() {
|
||||
export default function MainApp() {
|
||||
useMaybeMigrateSettings();
|
||||
|
||||
const { data: isAuthed, isFetching: isFetchingAuth } = useIsAuthed();
|
||||
const { gitHubToken } = useAuth();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(
|
||||
!localStorage.getItem("analytics-consent"),
|
||||
);
|
||||
|
||||
const { data: settings } = useSettings();
|
||||
const config = useConfig();
|
||||
const { data: isAuthed, isFetching: isFetchingAuth } = useIsAuthed();
|
||||
|
||||
const gitHubAuthUrl = useGitHubAuthUrl({
|
||||
gitHubToken,
|
||||
|
||||
@@ -7,24 +7,19 @@ import { useAuth } from "#/context/auth-context";
|
||||
function OAuthGitHubCallback() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { setAccessTokens, setUserId } = useAuth();
|
||||
const { setGitHubToken } = useAuth();
|
||||
|
||||
const code = searchParams.get("code");
|
||||
const requesterUrl = new URL(window.location.href);
|
||||
const redirectUrl = `${requesterUrl.origin}/oauth/github/callback`;
|
||||
|
||||
const { data, isSuccess, error } = useQuery({
|
||||
queryKey: ["access_token", code, redirectUrl],
|
||||
queryFn: () => OpenHands.getGitHubAccessToken(code!, redirectUrl),
|
||||
queryKey: ["access_token", code],
|
||||
queryFn: () => OpenHands.getGitHubAccessToken(code!),
|
||||
enabled: !!code,
|
||||
});
|
||||
|
||||
console.debug(`data: ${JSON.stringify(data)}, isSuccess: ${isSuccess}`);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isSuccess) {
|
||||
setAccessTokens(data.providerAccessToken, data.keycloakAccessToken);
|
||||
setUserId(data.keycloakUserId);
|
||||
setGitHubToken(data.access_token);
|
||||
navigate("/");
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
*/
|
||||
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
|
||||
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
|
||||
// const scope = "repo,user,workflow,offline_access";
|
||||
console.debug(
|
||||
`http://localhost:8080/realms/allhandsgithub/protocol/openid-connect/auth?client_id=allhandsgithub&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=openid+email+profile&state=some-state-value&nonce=222`,
|
||||
);
|
||||
// return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
|
||||
return `http://localhost:8080/realms/allhandsgithub/protocol/openid-connect/auth?client_id=allhandsgithub&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=openid+email+profile&state=some-state-value&nonce=222`;
|
||||
const scope = "repo,user,workflow,offline_access";
|
||||
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ IPythonTool = ChatCompletionToolParam(
|
||||
),
|
||||
)
|
||||
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file.
|
||||
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
|
||||
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections.
|
||||
* IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines.
|
||||
@@ -216,7 +216,7 @@ LLMBasedFileEditTool = ChatCompletionToolParam(
|
||||
),
|
||||
)
|
||||
|
||||
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
|
||||
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files
|
||||
* State is persistent across command calls and discussions with the user
|
||||
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
|
||||
* The `create` command cannot be used if the specified `path` already exists as a file
|
||||
|
||||
@@ -192,7 +192,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
|
||||
custom_fields[k] = v
|
||||
merged_llm_dict = generic_llm_fields.copy()
|
||||
merged_llm_dict.update(custom_fields)
|
||||
|
||||
|
||||
custom_llm_config = LLMConfig(**merged_llm_dict)
|
||||
cfg.set_llm_config(custom_llm_config, nested_key)
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class SensitiveDataFilter(logging.Filter):
|
||||
sensitive_patterns.extend(env_vars)
|
||||
|
||||
# and some special cases
|
||||
sensitive_patterns.append('JWT_SECRET_KEY')
|
||||
sensitive_patterns.append('JWT_SECRET')
|
||||
sensitive_patterns.append('LLM_API_KEY')
|
||||
sensitive_patterns.append('GITHUB_TOKEN')
|
||||
sensitive_patterns.append('SANDBOX_ENV_GITHUB_TOKEN')
|
||||
|
||||
@@ -9,7 +9,7 @@ app = APIRouter(prefix='/api/github')
|
||||
|
||||
|
||||
def require_github_token(request: Request):
|
||||
github_token = request.state.github_token
|
||||
github_token = request.headers.get('X-GitHub-Token')
|
||||
if not github_token:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
||||
@@ -11,7 +11,7 @@ def get_file_store(file_store: str, file_store_path: str | None = None) -> FileS
|
||||
raise ValueError('file_store_path is required for local file store')
|
||||
return LocalFileStore(file_store_path)
|
||||
elif file_store == 's3':
|
||||
return S3FileStore(file_store_path)
|
||||
return S3FileStore()
|
||||
elif file_store == 'google_cloud':
|
||||
return GoogleCloudFileStore(file_store_path)
|
||||
return InMemoryFileStore()
|
||||
|
||||
+23
-103
@@ -1,130 +1,50 @@
|
||||
import io
|
||||
import os
|
||||
|
||||
import boto3
|
||||
import botocore
|
||||
from minio import Minio
|
||||
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
class S3FileStore(FileStore):
|
||||
def __init__(self, bucket_name: str | None) -> None:
|
||||
def __init__(self) -> None:
|
||||
access_key = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
endpoint = os.getenv('AWS_S3_ENDPOINT', 's3.amazonaws.com')
|
||||
secure = os.getenv('AWS_S3_SECURE', 'true').lower() == 'true'
|
||||
endpoint = self._ensure_url_scheme(secure, os.getenv('AWS_S3_ENDPOINT'))
|
||||
if bucket_name is None:
|
||||
bucket_name = os.environ['AWS_S3_BUCKET']
|
||||
self.bucket = bucket_name
|
||||
self.client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
endpoint_url=endpoint,
|
||||
use_ssl=secure,
|
||||
)
|
||||
self.bucket = os.getenv('AWS_S3_BUCKET')
|
||||
self.client = Minio(endpoint, access_key, secret_key, secure=secure)
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
as_bytes = contents.encode('utf-8') if isinstance(contents, str) else contents
|
||||
stream = io.BytesIO(as_bytes)
|
||||
try:
|
||||
as_bytes = (
|
||||
contents.encode('utf-8') if isinstance(contents, str) else contents
|
||||
)
|
||||
self.client.put_object(Bucket=self.bucket, Key=path, Body=as_bytes)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to write to bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
self.client.put_object(self.bucket, path, stream, len(as_bytes))
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(f'Failed to write to S3 at path {path}: {e}')
|
||||
|
||||
def read(self, path: str) -> str:
|
||||
try:
|
||||
response = self.client.get_object(Bucket=self.bucket, Key=path)
|
||||
return response['Body'].read().decode('utf-8')
|
||||
except botocore.exceptions.ClientError as e:
|
||||
# Catch all S3-related errors
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
return self.client.get_object(self.bucket, path).data.decode('utf-8')
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
raise FileNotFoundError(f'Failed to read from S3 at path {path}: {e}')
|
||||
|
||||
def list(self, path: str) -> list[str]:
|
||||
if path and path != '/' and not path.endswith('/'):
|
||||
path += '/'
|
||||
try:
|
||||
response = self.client.list_objects_v2(Bucket=self.bucket, Prefix=path)
|
||||
# Check if 'Contents' exists in the response
|
||||
if 'Contents' in response:
|
||||
objects = [obj['Key'] for obj in response['Contents']]
|
||||
return objects
|
||||
else:
|
||||
return list()
|
||||
except botocore.exceptions.ClientError as e:
|
||||
# Catch all S3-related errors
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(f"Error: {e.response['Error']['Message']}")
|
||||
return [
|
||||
obj.object_name for obj in self.client.list_objects(self.bucket, path)
|
||||
]
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
)
|
||||
raise FileNotFoundError(f'Failed to list S3 objects at path {path}: {e}')
|
||||
|
||||
def delete(self, path: str) -> None:
|
||||
try:
|
||||
self.client.delete_object(Bucket=self.bucket, Key=path)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}': {e}"
|
||||
)
|
||||
client = self.client
|
||||
bucket = self.bucket
|
||||
objects_to_delete = client.list_objects(bucket, prefix=path, recursive=True)
|
||||
for obj in objects_to_delete:
|
||||
client.remove_object(bucket, obj.object_name)
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}: {e}"
|
||||
)
|
||||
|
||||
def _ensure_url_scheme(self, secure: bool, url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
if secure:
|
||||
if not url.startswith('https://'):
|
||||
url = 'https://' + url.removeprefix('http://')
|
||||
else:
|
||||
if not url.startswith('http://'):
|
||||
url = 'http://' + url.removeprefix('https://')
|
||||
return url
|
||||
raise FileNotFoundError(f'Failed to delete S3 object at path {path}: {e}')
|
||||
|
||||
Generated
+406
-831
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,6 @@ openhands-aci = "0.1.8"
|
||||
python-socketio = "^5.11.4"
|
||||
redis = "^5.2.0"
|
||||
sse-starlette = "^2.1.3"
|
||||
pre-commit = "^4.0.1"
|
||||
|
||||
[tool.poetry.group.llama-index.dependencies]
|
||||
llama-index = "*"
|
||||
|
||||
Reference in New Issue
Block a user