Compare commits

..

10 Commits

Author SHA1 Message Date
openhands 0ce7f84f62 back to setting modal 2024-11-09 02:50:14 +00:00
openhands 3336b4f988 style: fix extra newlines in settings form 2024-11-09 02:37:53 +00:00
openhands ac9be85682 refactor: move browser control toggle to browser tab
- Move browser control toggle from settings form to browser tab
- Add toggle in browser tab header next to URL
- Keep same functionality but improve discoverability
2024-11-09 02:28:28 +00:00
openhands ef8a06dd51 test: update settings test to include ENABLE_BROWSING 2024-11-09 02:09:54 +00:00
openhands faf60d7246 fix: make browser control disabled by default
- Change default value of ENABLE_BROWSING to false in frontend settings
- Change default value of codeact_enable_browsing to false in agent config
- Update session initialization to default to disabled browsing
2024-11-09 02:02:08 +00:00
openhands 6c0b36271d feat: add frontend support for browser control toggle
- Add ENABLE_BROWSING to Settings type
- Add browser control toggle to settings form
- Update settings route to handle browser toggle
2024-11-09 01:59:30 +00:00
openhands 0386a96f72 feat: add browser control toggle in settings
- Add ENABLE_BROWSING setting to control browser functionality
- Update agent config to use the browser toggle setting
- Update session initialization to handle browser toggle
2024-11-09 01:56:24 +00:00
Robert Brennan 35c68863dc Don't persist cache on reload (#4854) 2024-11-08 22:31:24 +00:00
mamoodi 8bfee87bcf Release 0.13.0 (#4849) 2024-11-08 22:24:56 +00:00
Robert Brennan e1383afbc3 Add signed cookie-based GitHub authentication caching (#4853)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-08 22:19:34 +00:00
22 changed files with 133 additions and 93 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
### 9. Use existing Docker image ### 9. Use existing Docker image
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image. Follow these steps: To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image. Follow these steps:
1. Set the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image. 1. Set the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.12-nikolaik 2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.13-nikolaik
## Develop inside Docker container ## Develop inside Docker container
+3 -3
View File
@@ -38,15 +38,15 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
system requirements and more information. system requirements and more information.
```bash ```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
docker run -it --pull=always \ docker run -it --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.12 docker.all-hands.dev/all-hands-ai/openhands:0.13
``` ```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)! You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest image: openhands:latest
container_name: openhands-app-${DATE:-} container_name: openhands-app-${DATE:-}
environment: environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.12-nikolaik} - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.13-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} - SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports: ports:
+1 -1
View File
@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"} - BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal - SANDBOX_API_HOSTNAME=host.docker.internal
# #
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.12-nikolaik} - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.13-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} - SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports: ports:
+2 -2
View File
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \ -e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.12 \ docker.all-hands.dev/all-hands-ai/openhands:0.13 \
python -m openhands.core.cli python -m openhands.core.cli
``` ```
+2 -2
View File
@@ -44,7 +44,7 @@ LLM_API_KEY="sk_test_12345"
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \ -e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -53,6 +53,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.12 \ docker.all-hands.dev/all-hands-ai/openhands:0.13 \
python -m openhands.core.main -t "write a bash script that prints hi" python -m openhands.core.main -t "write a bash script that prints hi"
``` ```
+3 -3
View File
@@ -11,15 +11,15 @@
The easiest way to run OpenHands is in Docker. The easiest way to run OpenHands is in Docker.
```bash ```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik docker pull docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik
docker run -it --rm --pull=always \ docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.12-nikolaik \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.12 docker.all-hands.dev/all-hands-ai/openhands:0.13
``` ```
You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action). You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
+5 -1
View File
@@ -22,7 +22,8 @@ describe("getSettings", () => {
.mockReturnValueOnce("language_value") .mockReturnValueOnce("language_value")
.mockReturnValueOnce("api_key") .mockReturnValueOnce("api_key")
.mockReturnValueOnce("true") .mockReturnValueOnce("true")
.mockReturnValueOnce("invariant"); .mockReturnValueOnce("invariant")
.mockReturnValueOnce("true");
const settings = getSettings(); const settings = getSettings();
@@ -34,6 +35,7 @@ describe("getSettings", () => {
LLM_API_KEY: "api_key", LLM_API_KEY: "api_key",
CONFIRMATION_MODE: true, CONFIRMATION_MODE: true,
SECURITY_ANALYZER: "invariant", SECURITY_ANALYZER: "invariant",
ENABLE_BROWSING: true,
}); });
}); });
@@ -58,6 +60,7 @@ describe("getSettings", () => {
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL, LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE, CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER, SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER,
ENABLE_BROWSING: DEFAULT_SETTINGS.ENABLE_BROWSING,
}); });
}); });
}); });
@@ -72,6 +75,7 @@ describe("saveSettings", () => {
LLM_API_KEY: "some_key", LLM_API_KEY: "some_key",
CONFIRMATION_MODE: true, CONFIRMATION_MODE: true,
SECURITY_ANALYZER: "invariant", SECURITY_ANALYZER: "invariant",
ENABLE_BROWSING: true,
}; };
saveSettings(settings); saveSettings(settings);
+6 -27
View File
@@ -8,7 +8,6 @@ describe("Cache", () => {
const testTTL = 1000; // 1 second const testTTL = 1000; // 1 second
beforeEach(() => { beforeEach(() => {
localStorage.clear();
vi.useFakeTimers(); vi.useFakeTimers();
}); });
@@ -16,17 +15,7 @@ describe("Cache", () => {
vi.useRealTimers(); vi.useRealTimers();
}); });
it("sets data in localStorage with expiration", () => { it("gets data from memory if not expired", () => {
cache.set(testKey, testData, testTTL);
const cachedEntry = JSON.parse(
localStorage.getItem(`app_cache_${testKey}`) || "",
);
expect(cachedEntry.data).toEqual(testData);
expect(cachedEntry.expiration).toBeGreaterThan(Date.now());
});
it("gets data from localStorage if not expired", () => {
cache.set(testKey, testData, testTTL); cache.set(testKey, testData, testTTL);
expect(cache.get(testKey)).toEqual(testData); expect(cache.get(testKey)).toEqual(testData);
@@ -39,7 +28,6 @@ describe("Cache", () => {
vi.advanceTimersByTime(5 * 60 * 1000 + 1); vi.advanceTimersByTime(5 * 60 * 1000 + 1);
expect(cache.get(testKey)).toBeNull(); expect(cache.get(testKey)).toBeNull();
expect(localStorage.getItem(`app_cache_${testKey}`)).toBeNull();
}); });
it("returns null if cached data is expired", () => { it("returns null if cached data is expired", () => {
@@ -47,28 +35,19 @@ describe("Cache", () => {
vi.advanceTimersByTime(testTTL + 1); vi.advanceTimersByTime(testTTL + 1);
expect(cache.get(testKey)).toBeNull(); expect(cache.get(testKey)).toBeNull();
expect(localStorage.getItem(`app_cache_${testKey}`)).toBeNull();
}); });
it("deletes data from localStorage", () => { it("deletes data from memory", () => {
cache.set(testKey, testData, testTTL); cache.set(testKey, testData, testTTL);
cache.delete(testKey); cache.delete(testKey);
expect(cache.get(testKey)).toBeNull();
expect(localStorage.getItem(`app_cache_${testKey}`)).toBeNull();
}); });
it("clears all data with the app prefix from localStorage", () => { it("clears all data with the app prefix from memory", () => {
cache.set(testKey, testData, testTTL); cache.set(testKey, testData, testTTL);
cache.set("anotherKey", { data: "More data" }, testTTL); cache.set("anotherKey", { data: "More data" }, testTTL);
cache.clearAll(); cache.clearAll();
expect(cache.get(testKey)).toBeNull();
expect(localStorage.length).toBe(0); expect(cache.get("anotherKey")).toBeNull();
});
it("does not retrieve non-prefixed data from localStorage when clearing", () => {
localStorage.setItem("nonPrefixedKey", "should remain");
cache.set(testKey, testData, testTTL);
cache.clearAll();
expect(localStorage.getItem("nonPrefixedKey")).toBe("should remain");
}); });
}); });
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "openhands-frontend", "name": "openhands-frontend",
"version": "0.12.3", "version": "0.13.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openhands-frontend", "name": "openhands-frontend",
"version": "0.12.3", "version": "0.13.0",
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8", "@nextui-org/react": "^2.4.8",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "openhands-frontend", "name": "openhands-frontend",
"version": "0.12.3", "version": "0.13.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
@@ -120,4 +120,4 @@
"public" "public"
] ]
} }
} }
+20 -1
View File
@@ -1,16 +1,24 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IoIosGlobe } from "react-icons/io"; import { IoIosGlobe } from "react-icons/io";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Switch } from "@nextui-org/react";
import clsx from "clsx";
import { I18nKey } from "#/i18n/declaration"; import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store"; import { RootState } from "#/store";
import { getSettings, saveSettings } from "#/services/settings";
function BrowserPanel() { function BrowserPanel() {
const { t } = useTranslation(); const { t } = useTranslation();
const settings = getSettings();
const { url, screenshotSrc } = useSelector( const { url, screenshotSrc } = useSelector(
(state: RootState) => state.browser, (state: RootState) => state.browser,
); );
const handleBrowserToggle = (enabled: boolean) => {
saveSettings({ ...settings, ENABLE_BROWSING: enabled });
};
const imgSrc = const imgSrc =
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,") screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
? screenshotSrc ? screenshotSrc
@@ -22,7 +30,18 @@ function BrowserPanel() {
{url} {url}
</div> </div>
<div className="overflow-y-auto grow scrollbar-hide rounded-xl"> <div className="overflow-y-auto grow scrollbar-hide rounded-xl">
{screenshotSrc ? ( {!settings.ENABLE_BROWSING ? (
<div className="flex flex-col items-center h-full justify-center gap-4 text-center px-4">
<IoIosGlobe size={100} />
<div>
<p className="text-lg mb-2">Browser Control is Disabled</p>
<p className="text-sm text-neutral-500">
Browser control is an experimental feature that allows the AI assistant to interact with web browsers.
To enable it, go to Settings and enable Browser Control.
</p>
</div>
</div>
) : screenshotSrc ? (
<img <img
src={imgSrc} src={imgSrc}
style={{ objectFit: "contain", width: "100%", height: "auto" }} style={{ objectFit: "contain", width: "100%", height: "auto" }}
@@ -249,6 +249,25 @@ export function SettingsForm({
</p> </p>
</fieldset> </fieldset>
<Switch
isDisabled={disabled}
name="enable-browsing"
defaultSelected={settings.ENABLE_BROWSING}
classNames={{
thumb: clsx(
"bg-[#5D5D5D] w-3 h-3",
"group-data-[selected=true]:bg-white",
),
wrapper: clsx(
"border border-[#D4D4D4] bg-white px-[6px] w-12 h-6",
"group-data-[selected=true]:border-transparent group-data-[selected=true]:bg-[#4465DB]",
),
label: "text-[#A3A3A3] text-xs",
}}
>
Browser Control (Experimental)
</Switch>
{showAdvancedOptions && ( {showAdvancedOptions && (
<fieldset <fieldset
data-testid="agent-selector" data-testid="agent-selector"
+3
View File
@@ -50,11 +50,13 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
let baseUrl: string | undefined; let baseUrl: string | undefined;
let confirmationMode = false; let confirmationMode = false;
let securityAnalyzer: string | undefined; let securityAnalyzer: string | undefined;
let enableBrowsing = true;
if (isUsingAdvancedOptions) { if (isUsingAdvancedOptions) {
customModel = formData.get("custom-model")?.toString(); customModel = formData.get("custom-model")?.toString();
baseUrl = formData.get("base-url")?.toString(); baseUrl = formData.get("base-url")?.toString();
confirmationMode = keys.includes("confirmation-mode"); confirmationMode = keys.includes("confirmation-mode");
enableBrowsing = keys.includes("enable-browsing");
if (confirmationMode) { if (confirmationMode) {
// only set securityAnalyzer if confirmationMode is enabled // only set securityAnalyzer if confirmationMode is enabled
securityAnalyzer = formData.get("security-analyzer")?.toString(); securityAnalyzer = formData.get("security-analyzer")?.toString();
@@ -80,6 +82,7 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
LLM_BASE_URL, LLM_BASE_URL,
CONFIRMATION_MODE, CONFIRMATION_MODE,
SECURITY_ANALYZER, SECURITY_ANALYZER,
ENABLE_BROWSING: enableBrowsing,
}; };
saveSettings(settings); saveSettings(settings);
+10
View File
@@ -63,6 +63,16 @@ export async function request(
} catch (e) { } catch (e) {
onFail(`Error fetching ${url}`); onFail(`Error fetching ${url}`);
} }
if (response?.status === 401) {
await request(
"/api/authenticate",
{
method: "POST",
},
true,
);
return request(url, options, disableToast, returnResponse, maxRetries - 1);
}
if (response?.status && response?.status >= 400) { if (response?.status && response?.status >= 400) {
onFail( onFail(
`${response.status} error while fetching ${url}: ${response?.statusText}`, `${response.status} error while fetching ${url}: ${response?.statusText}`,
+4
View File
@@ -8,6 +8,7 @@ export type Settings = {
LLM_API_KEY: string; LLM_API_KEY: string;
CONFIRMATION_MODE: boolean; CONFIRMATION_MODE: boolean;
SECURITY_ANALYZER: string; SECURITY_ANALYZER: string;
ENABLE_BROWSING: boolean;
}; };
export const DEFAULT_SETTINGS: Settings = { export const DEFAULT_SETTINGS: Settings = {
@@ -18,6 +19,7 @@ export const DEFAULT_SETTINGS: Settings = {
LLM_API_KEY: "", LLM_API_KEY: "",
CONFIRMATION_MODE: false, CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "", SECURITY_ANALYZER: "",
ENABLE_BROWSING: false,
}; };
const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[]; const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[];
@@ -71,6 +73,7 @@ export const getSettings = (): Settings => {
const apiKey = localStorage.getItem("LLM_API_KEY"); const apiKey = localStorage.getItem("LLM_API_KEY");
const confirmationMode = localStorage.getItem("CONFIRMATION_MODE") === "true"; const confirmationMode = localStorage.getItem("CONFIRMATION_MODE") === "true";
const securityAnalyzer = localStorage.getItem("SECURITY_ANALYZER"); const securityAnalyzer = localStorage.getItem("SECURITY_ANALYZER");
const enableBrowsing = localStorage.getItem("ENABLE_BROWSING") === "true"; // Default to false if not set
return { return {
LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL, LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL,
@@ -80,6 +83,7 @@ export const getSettings = (): Settings => {
LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY, LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY,
CONFIRMATION_MODE: confirmationMode || DEFAULT_SETTINGS.CONFIRMATION_MODE, CONFIRMATION_MODE: confirmationMode || DEFAULT_SETTINGS.CONFIRMATION_MODE,
SECURITY_ANALYZER: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER, SECURITY_ANALYZER: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
ENABLE_BROWSING: enableBrowsing,
}; };
}; };
+15 -24
View File
@@ -5,26 +5,17 @@ type CacheEntry<T> = {
}; };
class Cache { class Cache {
private prefix = "app_cache_";
private defaultTTL = 5 * 60 * 1000; // 5 minutes private defaultTTL = 5 * 60 * 1000; // 5 minutes
/** private cacheMemory: Record<string, string> = {};
* Generate a unique key with prefix for local storage
* @param key The key to be stored in local storage
* @returns The unique key with prefix
*/
private getKey(key: CacheKey): string {
return `${this.prefix}${key}`;
}
/** /**
* Retrieve the cached data from local storage * Retrieve the cached data from memory
* @param key The key to be retrieved from local storage * @param key The key to be retrieved from memory
* @returns The data stored in local storage * @returns The data stored in memory
*/ */
public get<T>(key: CacheKey): T | null { public get<T>(key: CacheKey): T | null {
const cachedEntry = localStorage.getItem(this.getKey(key)); const cachedEntry = this.cacheMemory[key];
if (cachedEntry) { if (cachedEntry) {
const { data, expiration } = JSON.parse(cachedEntry) as CacheEntry<T>; const { data, expiration } = JSON.parse(cachedEntry) as CacheEntry<T>;
if (Date.now() < expiration) return data; if (Date.now() < expiration) return data;
@@ -35,34 +26,34 @@ class Cache {
} }
/** /**
* Store the data in local storage with expiration * Store the data in memory with expiration
* @param key The key to be stored in local storage * @param key The key to be stored in memory
* @param data The data to be stored in local storage * @param data The data to be stored in memory
* @param ttl The time to live for the data in milliseconds * @param ttl The time to live for the data in milliseconds
* @returns void * @returns void
*/ */
public set<T>(key: CacheKey, data: T, ttl = this.defaultTTL): void { public set<T>(key: CacheKey, data: T, ttl = this.defaultTTL): void {
const expiration = Date.now() + ttl; const expiration = Date.now() + ttl;
const entry: CacheEntry<T> = { data, expiration }; const entry: CacheEntry<T> = { data, expiration };
localStorage.setItem(this.getKey(key), JSON.stringify(entry)); this.cacheMemory[key] = JSON.stringify(entry);
} }
/** /**
* Remove the data from local storage * Remove the data from memory
* @param key The key to be removed from local storage * @param key The key to be removed from memory
* @returns void * @returns void
*/ */
public delete(key: CacheKey): void { public delete(key: CacheKey): void {
localStorage.removeItem(this.getKey(key)); delete this.cacheMemory[key];
} }
/** /**
* Clear all data with the app prefix from local storage * Clear all data
* @returns void * @returns void
*/ */
public clearAll(): void { public clearAll(): void {
Object.keys(localStorage).forEach((key) => { Object.keys(this.cacheMemory).forEach((key) => {
if (key.startsWith(this.prefix)) localStorage.removeItem(key); delete this.cacheMemory[key];
}); });
} }
} }
+1 -1
View File
@@ -19,7 +19,7 @@ class AgentConfig:
""" """
function_calling: bool = True function_calling: bool = True
codeact_enable_browsing: bool = True codeact_enable_browsing: bool = False
codeact_enable_llm_editor: bool = False codeact_enable_llm_editor: bool = False
codeact_enable_jupyter: bool = True codeact_enable_jupyter: bool = True
micro_agent_name: str | None = None micro_agent_name: str | None = None
+1
View File
@@ -47,3 +47,4 @@ class ConfigType(str, Enum):
WORKSPACE_MOUNT_PATH = 'WORKSPACE_MOUNT_PATH' WORKSPACE_MOUNT_PATH = 'WORKSPACE_MOUNT_PATH'
WORKSPACE_MOUNT_PATH_IN_SANDBOX = 'WORKSPACE_MOUNT_PATH_IN_SANDBOX' WORKSPACE_MOUNT_PATH_IN_SANDBOX = 'WORKSPACE_MOUNT_PATH_IN_SANDBOX'
WORKSPACE_MOUNT_REWRITE = 'WORKSPACE_MOUNT_REWRITE' WORKSPACE_MOUNT_REWRITE = 'WORKSPACE_MOUNT_REWRITE'
ENABLE_BROWSING = 'ENABLE_BROWSING'
+27 -18
View File
@@ -2,10 +2,12 @@ import asyncio
import os import os
import re import re
import tempfile import tempfile
import time
import uuid import uuid
import warnings import warnings
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import jwt
import requests import requests
from pathspec import PathSpec from pathspec import PathSpec
from pathspec.patterns import GitWildMatchPattern from pathspec.patterns import GitWildMatchPattern
@@ -15,6 +17,7 @@ from openhands.server.data_models.feedback import FeedbackDataModel, store_feedb
from openhands.server.github import ( from openhands.server.github import (
GITHUB_CLIENT_ID, GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET, GITHUB_CLIENT_SECRET,
UserVerifier,
authenticate_github_user, authenticate_github_user,
) )
from openhands.storage import get_file_store from openhands.storage import get_file_store
@@ -60,7 +63,7 @@ from openhands.events.serialization import event_to_dict
from openhands.events.stream import AsyncEventStreamWrapper from openhands.events.stream import AsyncEventStreamWrapper
from openhands.llm import bedrock from openhands.llm import bedrock
from openhands.runtime.base import Runtime from openhands.runtime.base import Runtime
from openhands.server.auth import get_sid_from_token, sign_token from openhands.server.auth.auth import get_sid_from_token, sign_token
from openhands.server.middleware import LocalhostCORSMiddleware, NoCacheMiddleware from openhands.server.middleware import LocalhostCORSMiddleware, NoCacheMiddleware
from openhands.server.session import SessionManager from openhands.server.session import SessionManager
@@ -204,23 +207,21 @@ async def attach_session(request: Request, call_next):
response = await call_next(request) response = await call_next(request)
return response return response
# First check for auth cookie user_verifier = UserVerifier()
github_token = request.cookies.get('github_auth') if user_verifier.is_active():
signed_token = request.cookies.get('github_auth')
# If no cookie, fall back to header if not signed_token:
if not github_token:
github_token = request.headers.get('X-GitHub-Token')
# If no header token either, return error
if not github_token:
return JSONResponse( return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
content={'error': 'Not authenticated'}, content={'error': 'Not authenticated'},
) )
# If using header token, verify with GitHub try:
if not await authenticate_github_user(github_token): jwt.decode(signed_token, config.jwt_secret, algorithms=['HS256'])
except Exception as e:
logger.warning(f'Invalid token: {e}')
return JSONResponse( return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
content={'error': 'Not authenticated'}, content={'error': 'Invalid token'},
) )
if not request.headers.get('Authorization'): if not request.headers.get('Authorization'):
@@ -876,17 +877,25 @@ async def authenticate(request: Request):
content={'error': 'Not authorized via GitHub waitlist'}, content={'error': 'Not authorized via GitHub waitlist'},
) )
# Create a signed JWT token with 1-hour expiration
cookie_data = {
'github_token': token,
'exp': int(time.time()) + 3600, # 1 hour expiration
}
signed_token = sign_token(cookie_data, config.jwt_secret)
response = JSONResponse( response = JSONResponse(
status_code=status.HTTP_200_OK, content={'message': 'User authenticated'}) status_code=status.HTTP_200_OK, content={'message': 'User authenticated'}
)
# Set secure cookie that expires in 1 hour
# Set secure cookie with signed token
response.set_cookie( response.set_cookie(
key="github_auth", key='github_auth',
value=token, value=signed_token,
max_age=3600, # 1 hour in seconds max_age=3600, # 1 hour in seconds
httponly=True, httponly=True,
secure=True, secure=True,
samesite="strict" samesite='strict',
) )
return response return response
+4 -1
View File
@@ -89,6 +89,10 @@ class Session:
ConfigType.SECURITY_ANALYZER, self.config.security.security_analyzer ConfigType.SECURITY_ANALYZER, self.config.security.security_analyzer
) )
max_iterations = args.get(ConfigType.MAX_ITERATIONS, self.config.max_iterations) max_iterations = args.get(ConfigType.MAX_ITERATIONS, self.config.max_iterations)
# Get agent config and update browsing setting
agent_config = self.config.get_agent_config(agent_cls)
agent_config.codeact_enable_browsing = args.get('ENABLE_BROWSING', False)
# override default LLM config # override default LLM config
default_llm_config = self.config.get_llm_config() default_llm_config = self.config.get_llm_config()
default_llm_config.model = args.get( default_llm_config.model = args.get(
@@ -104,7 +108,6 @@ class Session:
# TODO: override other LLM config & agent config groups (#2075) # TODO: override other LLM config & agent config groups (#2075)
llm = LLM(config=self.config.get_llm_config_from_agent(agent_cls)) llm = LLM(config=self.config.get_llm_config_from_agent(agent_cls))
agent_config = self.config.get_agent_config(agent_cls)
agent = Agent.get_cls(agent_cls)(llm, agent_config) agent = Agent.get_cls(agent_cls)(llm, agent_config)
# Create the agent session # Create the agent session
+1 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "openhands-ai" name = "openhands-ai"
version = "0.12.3" version = "0.13.0"
description = "OpenHands: Code Less, Make More" description = "OpenHands: Code Less, Make More"
authors = ["OpenHands"] authors = ["OpenHands"]
license = "MIT" license = "MIT"
@@ -92,7 +92,6 @@ reportlab = "*"
[tool.coverage.run] [tool.coverage.run]
concurrency = ["gevent"] concurrency = ["gevent"]
[tool.poetry.group.runtime.dependencies] [tool.poetry.group.runtime.dependencies]
jupyterlab = "*" jupyterlab = "*"
notebook = "*" notebook = "*"
@@ -123,7 +122,6 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle] [tool.ruff.lint.pydocstyle]
convention = "google" convention = "google"
[tool.poetry.group.evaluation.dependencies] [tool.poetry.group.evaluation.dependencies]
streamlit = "*" streamlit = "*"
whatthepatch = "*" whatthepatch = "*"