mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ce7f84f62 | |||
| 3336b4f988 | |||
| ac9be85682 | |||
| ef8a06dd51 | |||
| faf60d7246 | |||
| 6c0b36271d | |||
| 0386a96f72 | |||
| 35c68863dc | |||
| 8bfee87bcf | |||
| e1383afbc3 |
+1
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+2
-2
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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
@@ -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];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 = "*"
|
||||||
|
|||||||
Reference in New Issue
Block a user