Compare commits

...

68 Commits

Author SHA1 Message Date
Chuck Butkus 75a4033bef Update dockerfile 2025-08-27 01:42:18 -04:00
Chuck Butkus 2f9a4f96ae Fix dockerfile 2025-08-26 19:18:00 -04:00
chuckbutkus ac152da39e Merge branch 'main' into non-root-user 2025-08-26 14:25:39 -04:00
Tim O'Farrell 4a4f213f57 Remove unused translation keys from translation.json (#10631)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 11:59:48 -06:00
Chuck Butkus 77210bc678 Revert "Fix failing Docker-dependent tests in test_runtime_build.py"
This reverts commit 39049b173a.
2025-08-26 13:08:38 -04:00
Chuck Butkus 425b4f77a9 Merge branch 'main' into non-root-user 2025-08-26 12:57:00 -04:00
Tim O'Farrell f9099fe6db Refactor conversation status (#10590)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 08:06:26 -06:00
Xingyao Wang 8f46a0a7a3 Add gpt-5-mini-2025-08-07 as verified model & supported in OpenHands provider (#10628)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 13:15:16 +00:00
dependabot[bot] 55d204ae1b chore(deps): bump the version-all group in /frontend with 21 updates (#10614)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 06:09:20 +00:00
baii 4d7cd228da Fix(backend): correctly forward AWS Bedrock aws_access_key_id / aws_secret_access_key / aws_region_name to litellm (#9663)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-25 22:07:28 +00:00
Tim O'Farrell a3f92df4b3 Fix for issue where exceptions are swallowed (#10602) 2025-08-25 15:50:15 -06:00
Engel Nyst e41f8f5215 feat(settings): configurable condenser max history size (FE+BE) (#10591) 2025-08-25 22:50:52 +02:00
Jamie Chicago 6448f5a681 docs: Add Ubuntu installation steps for Windows WSL setup (#10485)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-25 20:43:04 +00:00
chuckbutkus a84670e6bf Merge branch 'main' into non-root-user 2025-08-18 17:39:39 -04:00
chuckbutkus fc3b3f733f Merge branch 'main' into non-root-user 2025-08-15 17:28:04 -04:00
chuckbutkus 150f56f252 Merge branch 'main' into non-root-user 2025-08-13 12:39:17 -04:00
chuckbutkus 05171f08fb Merge branch 'main' into non-root-user 2025-08-13 00:28:53 -04:00
chuckbutkus 64a160cf03 Merge branch 'main' into non-root-user 2025-08-12 23:12:11 -04:00
chuckbutkus 8b1f38e52e Merge branch 'main' into non-root-user 2025-08-12 15:44:52 -04:00
Chuck Butkus 868f434f97 Fix logging 2025-08-12 12:49:30 -04:00
Chuck Butkus 210f7fc653 Lint fix 2025-08-12 12:40:03 -04:00
Chuck Butkus f9512cd234 Move logging to debug 2025-08-12 12:31:57 -04:00
chuckbutkus ff8e659905 Merge branch 'main' into non-root-user 2025-08-12 12:17:54 -04:00
chuckbutkus c32610a440 Merge branch 'main' into non-root-user 2025-08-11 20:40:40 -04:00
Chuck Butkus 26abb81a86 Fix merge 2025-08-11 16:51:56 -04:00
Chuck Butkus 23121301eb Revert "Fix circular import by moving refine_prompt to platform_utils"
This reverts commit e075873962.
2025-08-11 16:50:18 -04:00
chuckbutkus 97cd7eb0a2 Merge branch 'main' into non-root-user 2025-08-11 16:42:35 -04:00
Chuck Butkus 12e8183336 Revert "test"
This reverts commit b69b01cc15.
2025-08-11 16:42:18 -04:00
openhands e075873962 Fix circular import by moving refine_prompt to platform_utils
- Created new openhands/utils/platform_utils.py module for platform-specific utilities
- Moved refine_prompt function from openhands/agenthub/codeact_agent/tools/bash.py to platform_utils
- Updated imports in openhands/utils/prompt.py and bash.py to use the new location
- Resolves circular import: utils.prompt -> agenthub -> codeact_agent -> memory.conversation_memory -> utils.prompt

This fixes the ImportError when starting pods:
'cannot import name ConversationInstructions from partially initialized module openhands.utils.prompt'
2025-08-11 06:13:28 +00:00
Chuck Butkus ae1288a3e8 revert 2025-08-11 02:04:31 -04:00
Chuck Butkus 3a2327a879 Fix circular reference 2025-08-11 01:05:03 -04:00
Chuck Butkus d1f40ffab1 test 2025-08-11 00:46:18 -04:00
Chuck Butkus 31241f2490 Change shell to bash 2025-08-11 00:24:53 -04:00
Chuck Butkus 08ffd53636 revert 2025-08-11 00:11:02 -04:00
openhands c88173c1df Fix Docker installation for Ubuntu 24.04 (noble) by using jammy repository 2025-08-11 04:05:02 +00:00
Chuck Butkus 5f8f4fd42c Another try 2025-08-10 23:57:45 -04:00
Chuck Butkus 310fa75535 Revert "Fix Dockerfile.j2"
This reverts commit a0dfbe39bf.
2025-08-10 23:56:46 -04:00
Chuck Butkus a0dfbe39bf Fix Dockerfile.j2 2025-08-10 23:52:30 -04:00
Chuck Butkus b69b01cc15 test 2025-08-10 23:33:07 -04:00
chuckbutkus acf8539a3f Merge branch 'main' into non-root-user 2025-08-10 23:19:14 -04:00
Chuck Butkus 08c6686026 Update Dockerfile 2025-08-10 22:52:39 -04:00
openhands 691f42cf77 Fix test assertions for COPY command with --chown flag
The Dockerfile generation now includes --chown=openhands:openhands in COPY commands,
but the test assertions were still expecting the old format without the chown flag.

Updated test assertions in:
- test_generate_dockerfile_build_from_scratch
- test_generate_dockerfile_build_from_lock
- test_generate_dockerfile_build_from_versioned

This fixes the CI test failures that were occurring due to the mismatch between
expected and actual Dockerfile content.
2025-08-10 05:47:04 +00:00
openhands 39049b173a Fix failing Docker-dependent tests in test_runtime_build.py 2025-08-10 05:31:00 +00:00
chuckbutkus 1b911ea5ba Merge branch 'main' into non-root-user 2025-08-10 01:04:53 -04:00
Chuck Butkus 971fcd7296 Lint fixes 2025-08-10 00:58:01 -04:00
Chuck Butkus a43778bf24 test 2025-08-10 00:10:35 -04:00
Chuck Butkus 67278772ad More logging 2025-08-09 22:47:49 -04:00
Chuck Butkus 33ec22ff91 More logging 2025-08-09 21:54:30 -04:00
Chuck Butkus c8dd141997 Logging 2025-08-09 15:18:45 -04:00
Chuck Butkus e2a56c9302 Fix su 2025-08-08 00:23:25 -04:00
Chuck Butkus 1d53587cbd Fix path 2025-08-07 23:51:58 -04:00
Chuck Butkus 880a798152 Update 2025-08-07 23:11:22 -04:00
chuckbutkus a356b09f30 Merge branch 'main' into non-root-user 2025-08-07 22:34:55 -04:00
Chuck Butkus 01e91cebe4 Update 2025-08-07 22:30:09 -04:00
Chuck Butkus 93cbba4e06 Another try 2025-08-07 22:05:15 -04:00
Chuck Butkus df45db5fa1 Try again 2025-08-07 21:16:58 -04:00
Chuck Butkus 36c628014f Update 2025-08-07 17:11:20 -04:00
chuckbutkus ff38146cb6 Merge branch 'main' into non-root-user 2025-08-07 15:58:29 -04:00
openhands 7512ffc16a Update Dockerfile.j2 to create openhands user and group with proper ownership 2025-08-07 16:27:22 +00:00
Chuck Butkus aa545c29f1 Revert "Try build for non-root"
This reverts commit 54cdc5c744.
2025-08-07 12:05:53 -04:00
Chuck Butkus 54cdc5c744 Try build for non-root 2025-08-07 00:46:01 -04:00
Chuck Butkus a5afc1ff7a Another fix 2025-08-06 23:50:35 -04:00
Chuck Butkus ecf23d3e74 Missed one 2025-08-06 23:48:27 -04:00
Chuck Butkus c2e080a340 Change linux group name 2025-08-06 23:42:13 -04:00
chuckbutkus 4b2ca6ca71 Merge branch 'main' into update-group-id 2025-08-06 16:58:35 -04:00
Chuck Butkus 42d1a54670 Add debug logging 2025-08-05 12:05:26 -04:00
Chuck Butkus 25261673a1 Fix runtime image creation to create user before using it. 2025-08-05 10:25:57 -04:00
Chuck Butkus d025d225ad Update user and group creation in Dockerfile 2025-08-05 09:16:08 -04:00
35 changed files with 670 additions and 504 deletions
+13 -13
View File
@@ -58,34 +58,34 @@ RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs
# Default is 60000, but we've seen up to 200000
RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs
RUN groupadd --gid $OPENHANDS_USER_ID app
RUN groupadd --gid $OPENHANDS_USER_ID openhands
RUN useradd -l -m -u $OPENHANDS_USER_ID --gid $OPENHANDS_USER_ID -s /bin/bash openhands && \
usermod -aG app openhands && \
usermod -aG openhands openhands && \
usermod -aG sudo openhands && \
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN chown -R openhands:app /app && chmod -R 770 /app
RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
RUN chown -R openhands:openhands /app && chmod -R 770 /app
RUN sudo chown -R openhands:openhands $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
USER openhands
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH" \
PYTHONPATH='/app'
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
COPY --chown=openhands:openhands --chmod=770 ./microagents ./microagents
COPY --chown=openhands:openhands --chmod=770 ./openhands ./openhands
COPY --chown=openhands:openhands --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:openhands pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
RUN python openhands/core/download.py # No-op to download assets
# Add this line to set group ownership of all files/directories not already in "app" group
# openhands:openhands -> openhands:app
RUN find /app \! -group app -exec chgrp app {} +
# openhands:openhands -> openhands:openhands
RUN find /app \! -group openhands -exec chgrp openhands {} +
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build
COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
COPY --chown=openhands:openhands --chmod=770 --from=frontend-builder /app/build ./frontend/build
COPY --chown=openhands:openhands --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
USER root
+8 -1
View File
@@ -45,6 +45,13 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
**Ubuntu (Linux Distribution)**
1. Install Ubuntu: `wsl --install -d Ubuntu` in PowerShell as Administrator.
2. Restart computer when prompted.
3. Open Ubuntu from Start menu to complete setup.
4. Verify installation: `wsl --list` should show Ubuntu.
**Docker Desktop**
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
@@ -53,7 +60,7 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
<Note>
The docker command below to start the app must be run inside the WSL terminal.
The docker command below to start the app must be run inside the WSL terminal. Use `wsl -d Ubuntu` in PowerShell or search "Ubuntu" in the Start menu to access the Ubuntu terminal.
</Note>
**Alternative: Windows without WSL**
+164 -271
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -11,17 +11,17 @@
"@heroui/use-infinite-scroll": "^2.2.10",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.8.0",
"@react-router/serve": "^7.8.0",
"@react-router/node": "^7.8.2",
"@react-router/serve": "^7.8.2",
"@react-types/shared": "^3.31.0",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/react-stripe-js": "^3.9.1",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.3",
"@tanstack/react-query": "^5.85.5",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.0",
"@vitejs/plugin-react": "^5.0.1",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.11.0",
@@ -29,32 +29,32 @@
"date-fns": "^4.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.12",
"i18next": "^25.3.6",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.539.0",
"isbot": "^5.1.30",
"jose": "^6.0.13",
"lucide-react": "^0.541.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.260.1",
"posthog-js": "^1.260.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.6.1",
"react-hot-toast": "^2.6.0",
"react-i18next": "^15.7.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.8.0",
"react-router": "^7.8.2",
"react-select": "^5.10.2",
"react-syntax-highlighter": "^15.6.1",
"react-syntax-highlighter": "^15.6.5",
"react-textarea-autosize": "^8.5.9",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.1.1",
"vite": "^7.1.3",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
},
@@ -88,16 +88,16 @@
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
"@react-router/dev": "^7.8.0",
"@playwright/test": "^1.55.0",
"@react-router/dev": "^7.8.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.2.0",
"@types/react": "^19.1.9",
"@types/node": "^24.3.0",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -126,7 +126,7 @@
"stripe": "^18.4.0",
"tailwindcss": "^4.1.8",
"typescript": "^5.9.2",
"vite-plugin-svgr": "^4.2.0",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.2"
},
@@ -23,9 +23,9 @@ export function ConfirmStopModal({
<ModalBackdrop>
<ModalBody className="items-start border border-tertiary">
<div className="flex flex-col gap-2">
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_STOP)} />
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_PAUSE)} />
<BaseModalDescription
description={t(I18nKey.CONVERSATION$STOP_WARNING)}
description={t(I18nKey.CONVERSATION$PAUSE_WARNING)}
/>
</div>
<div
@@ -129,7 +129,7 @@ export function ConversationCardContextMenu({
{onStop && (
<ContextMenuListItem testId="stop-button" onClick={onStop}>
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$STOP)} />
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$PAUSE)} />
</ContextMenuListItem>
)}
@@ -1,4 +1,6 @@
import { ConversationStatus } from "#/types/conversation-status";
import ArchivedIcon from "./state-indicators/archived.svg?react";
import ErrorIcon from "./state-indicators/error.svg?react";
import RunningIcon from "./state-indicators/running.svg?react";
import StartingIcon from "./state-indicators/starting.svg?react";
import StoppedIcon from "./state-indicators/stopped.svg?react";
@@ -9,6 +11,8 @@ const CONVERSATION_STATUS_INDICATORS: Record<ConversationStatus, SVGIcon> = {
STOPPED: StoppedIcon,
RUNNING: RunningIcon,
STARTING: StartingIcon,
ARCHIVED: ArchivedIcon,
ERROR: ErrorIcon,
};
interface ConversationStateIndicatorProps {
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#A7A9AC"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 7h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1 0 1.43-.98 2.63-2.31 2.98l1.46 1.46C20.88 15.61 22 13.95 22 12c0-2.76-2.24-5-5-5zm-1 4h-2.19l2 2H16zM2 4.27l3.11 3.11C3.29 8.12 2 9.91 2 12c0 2.76 2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1 0-1.59 1.21-2.9 2.76-3.07L8.73 11H8v2h2.73L13 15.27V17h1.73l4.01 4L20 19.74 3.27 3 2 4.27z"/><path d="M0 24V0" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 512 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e7000b"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>

After

Width:  |  Height:  |  Size: 254 B

@@ -19,6 +19,8 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
: settings.llm_api_key?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
condenser_max_size:
settings.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
provider_tokens_set: settings.PROVIDER_TOKENS_SET,
+2
View File
@@ -22,6 +22,8 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
CONDENSER_MAX_SIZE:
apiSettings.condenser_max_size ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
apiSettings.enable_proactive_conversation_starters,
+5
View File
@@ -97,6 +97,8 @@ export enum I18nKey {
SETTINGS$BASE_URL = "SETTINGS$BASE_URL",
SETTINGS$AGENT = "SETTINGS$AGENT",
SETTINGS$ENABLE_MEMORY_CONDENSATION = "SETTINGS$ENABLE_MEMORY_CONDENSATION",
SETTINGS$CONDENSER_MAX_SIZE = "SETTINGS$CONDENSER_MAX_SIZE",
SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP = "SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP",
SETTINGS$LANGUAGE = "SETTINGS$LANGUAGE",
ACTION$PUSH_TO_BRANCH = "ACTION$PUSH_TO_BRANCH",
ACTION$PUSH_CREATE_PR = "ACTION$PUSH_CREATE_PR",
@@ -326,6 +328,7 @@ export enum I18nKey {
USER$ACCOUNT_SETTINGS = "USER$ACCOUNT_SETTINGS",
JUPYTER$OUTPUT_LABEL = "JUPYTER$OUTPUT_LABEL",
BUTTON$STOP = "BUTTON$STOP",
BUTTON$PAUSE = "BUTTON$PAUSE",
BUTTON$EDIT_TITLE = "BUTTON$EDIT_TITLE",
BUTTON$DOWNLOAD_VIA_VSCODE = "BUTTON$DOWNLOAD_VIA_VSCODE",
BUTTON$DISPLAY_COST = "BUTTON$DISPLAY_COST",
@@ -337,6 +340,8 @@ export enum I18nKey {
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
CONVERSATION$CONFIRM_DELETE = "CONVERSATION$CONFIRM_DELETE",
CONVERSATION$CONFIRM_STOP = "CONVERSATION$CONFIRM_STOP",
CONVERSATION$CONFIRM_PAUSE = "CONVERSATION$CONFIRM_PAUSE",
CONVERSATION$PAUSE_WARNING = "CONVERSATION$PAUSE_WARNING",
CONVERSATION$STOP_WARNING = "CONVERSATION$STOP_WARNING",
CONVERSATION$METRICS_INFO = "CONVERSATION$METRICS_INFO",
CONVERSATION$CREATED = "CONVERSATION$CREATED",
+81 -81
View File
@@ -1551,6 +1551,38 @@
"de": "Speicherkondensation aktivieren",
"uk": "Увімкнути конденсацію пам'яті"
},
"SETTINGS$CONDENSER_MAX_SIZE": {
"en": "Memory condenser max history size",
"ja": "メモリ凝縮の最大履歴サイズ",
"zh-CN": "内存凝缩最大历史大小",
"zh-TW": "記憶體凝縮最大歷史大小",
"ko-KR": "메모리 응축 최대 기록 크기",
"no": "Maks historikkstørrelse for minnekondenser",
"it": "Dimensione massima cronologia condensatore di memoria",
"pt": "Tamanho máximo do histórico do condensador de memória",
"es": "Tamaño máximo del historial del condensador de memoria",
"ar": "الحد الأقصى لحجم سجل مكثف الذاكرة",
"fr": "Taille maximale de l'historique du condenseur de mémoire",
"tr": "Bellek yoğunlaştırıcı maksimum geçmiş boyutu",
"de": "Maximale Verlaufgröße des Speicherkondensators",
"uk": "Максимальний розмір історії конденсатора пам'яті"
},
"SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP": {
"en": "After this many events, the condenser will summarize history. Minimum 10.",
"ja": "このイベント数を超えると、凝縮器が履歴を要約します。最小 10。",
"zh-CN": "达到此事件数量后,凝缩器将汇总历史。最小 10。",
"zh-TW": "超過此事件數後,凝縮器會摘要歷史。最小 10。",
"ko-KR": "이 이벤트 수 이후 응축기가 기록을 요약합니다. 최소 10.",
"no": "Etter så mange hendelser vil kondenseren oppsummere historikken. Minimum 10.",
"it": "Dopo questo numero di eventi, il condensatore riassumerà la cronologia. Minimo 10.",
"pt": "Após esse número de eventos, o condensador irá resumir o histórico. Mínimo 10.",
"es": "Después de este número de eventos, el condensador resumirá el historial. Mínimo 10.",
"ar": "بعد هذا العدد من الأحداث، سيقوم المكثف بتلخيص السجل. الحد الأدنى 10.",
"fr": "Après ce nombre d'événements, le condenseur résumera l'historique. Minimum 10.",
"tr": "Bu kadar olaydan sonra yoğunlaştırıcı geçmişi özetler. En az 10.",
"de": "Nach so vielen Ereignissen fasst der Kondensator die Historie zusammen. Minimum 10.",
"uk": "Після цієї кількості подій конденсатор узагальнить історію. Мінімум 10."
},
"SETTINGS$LANGUAGE": {
"en": "Language",
"ja": "言語",
@@ -2063,22 +2095,6 @@
"de": "Git-Anbieter",
"uk": "Git-провайдер"
},
"ACCOUNT_SETTINGS$TITLE": {
"en": "Account Settings",
"ja": "アカウント設定",
"zh-CN": "账户设置",
"zh-TW": "帳戶設定",
"ko-KR": "계정 설정",
"no": "Kontoinnstillinger",
"it": "Impostazioni account",
"pt": "Configurações da conta",
"es": "Configuración de la cuenta",
"ar": "إعدادات الحساب",
"fr": "Paramètres du compte",
"tr": "Hesap ayarları",
"de": "Kontoeinstellungen",
"uk": "Налаштування облікового запису"
},
"WORKSPACE$TERMINAL_TAB_LABEL": {
"en": "Terminal",
"zh-CN": "终端",
@@ -5215,6 +5231,22 @@
"tr": "Durdur",
"uk": "Стоп"
},
"BUTTON$PAUSE": {
"en": "Pause",
"ja": "一時停止",
"zh-CN": "暂停",
"zh-TW": "暫停",
"ko-KR": "일시정지",
"fr": "Mettre en pause",
"es": "Pausar",
"de": "Pausieren",
"it": "Pausa",
"pt": "Pausar",
"ar": "إيقاف مؤقت",
"no": "Pause",
"tr": "Duraklat",
"uk": "Призупинити"
},
"BUTTON$EDIT_TITLE": {
"en": "Edit Title",
"ja": "タイトルを編集",
@@ -5391,8 +5423,40 @@
"de": "Stopp bestätigen",
"uk": "Підтвердити зупинку"
},
"CONVERSATION$CONFIRM_PAUSE": {
"en": "Confirm Pause",
"ja": "一時停止の確認",
"zh-CN": "确认暂停",
"zh-TW": "確認暫停",
"ko-KR": "일시정지 확인",
"no": "Bekreft pause",
"it": "Conferma pausa",
"pt": "Confirmar pausa",
"es": "Confirmar pausa",
"ar": "تأكيد الإيقاف المؤقت",
"fr": "Confirmer la mise en pause",
"tr": "Duraklatmayı Onayla",
"de": "Pause bestätigen",
"uk": "Підтвердити призупинення"
},
"CONVERSATION$PAUSE_WARNING": {
"en": "Are you sure you want to pause this conversation?",
"ja": "この会話を一時停止してもよろしいですか?",
"zh-CN": "您确定要暂停此对话吗?",
"zh-TW": "您確定要暫停此對話嗎?",
"ko-KR": "이 대화를 일시정지하시겠습니까?",
"no": "Er du sikker på at du vil pause denne samtalen?",
"it": "Sei sicuro di voler mettere in pausa questa conversazione?",
"pt": "Tem certeza de que deseja pausar esta conversa?",
"es": "¿Está seguro de que desea pausar esta conversación?",
"ar": "هل أنت متأكد أنك تريد إيقاف هذه المحادثة مؤقتًا؟",
"fr": "Êtes-vous sûr de vouloir mettre cette conversation en pause ?",
"tr": "Bu konuşmayı duraklatmak istediğinizden emin misiniz?",
"de": "Sind Sie sicher, dass Sie dieses Gespräch pausieren möchten?",
"uk": "Ви впевнені, що хочете призупинити цю розмову?"
},
"CONVERSATION$STOP_WARNING": {
"en": "Are you sure you want to stop this conversation?",
"en": "Are you sure you want to pause this conversation?",
"ja": "この会話を停止してもよろしいですか?",
"zh-CN": "您确定要停止此对话吗?",
"zh-TW": "您確定要停止此對話嗎?",
@@ -7583,22 +7647,6 @@
"tr": "Ajan görevine devam et",
"uk": "Відновити завдання агента"
},
"ACTION_BUTTON$PAUSE": {
"en": "Pause the current task",
"zh-CN": "暂停",
"zh-TW": "暫停",
"ko-KR": "일시정지",
"ja": "一時停止",
"no": "Sett gjeldende oppgave på pause",
"ar": "إيقاف المهمة الحالية مؤقتاً",
"de": "Aktuelle Aufgabe pausieren",
"fr": "Mettre en pause la tâche actuelle",
"it": "Metti in pausa il compito corrente",
"pt": "Pausar a tarefa atual",
"es": "Pausar la tarea actual",
"tr": "Mevcut görevi duraklat",
"uk": "Призупинити поточне завдання"
},
"BROWSER$SCREENSHOT_ALT": {
"en": "Browser Screenshot",
"zh-CN": "截图",
@@ -8207,22 +8255,6 @@
"tr": "Kullanıcı avatarı yer tutucusu",
"uk": "заповнювач аватара користувача"
},
"ACCOUNT_SETTINGS$SETTINGS": {
"en": "Account Settings",
"ja": "アカウント設定",
"zh-CN": "账户设置",
"zh-TW": "帳戶設定",
"ko-KR": "계정 설정",
"no": "Kontoinnstillinger",
"it": "Impostazioni account",
"pt": "Configurações da conta",
"es": "Configuración de la cuenta",
"ar": "إعدادات الحساب",
"fr": "Paramètres du compte",
"tr": "Hesap ayarları",
"de": "Kontoeinstellungen",
"uk": "Налаштування облікового запису"
},
"ACCOUNT_SETTINGS$LOGOUT": {
"en": "Logout",
"ja": "ログアウト",
@@ -9167,38 +9199,6 @@
"tr": "Bekleme listesine katıl",
"uk": "Приєднатися до списку очікування"
},
"ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS": {
"en": "Additional Settings",
"ja": "追加設定",
"zh-CN": "附加设置",
"zh-TW": "附加設定",
"ko-KR": "추가 설정",
"de": "Zusätzliche Einstellungen",
"no": "Ytterligere innstillinger",
"it": "Impostazioni aggiuntive",
"pt": "Configurações adicionais",
"es": "Configuraciones adicionales",
"ar": "إعدادات إضافية",
"fr": "Paramètres supplémentaires",
"tr": "Ek Ayarlar",
"uk": "Додаткові налаштування"
},
"ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB": {
"en": "Disconnect from GitHub",
"ja": "GitHubから切断",
"zh-CN": "断开与GitHub的连接",
"zh-TW": "中斷與GitHub的連接",
"ko-KR": "GitHub 연결 해제",
"de": "Von GitHub trennen",
"no": "Koble fra GitHub",
"it": "Disconnetti da GitHub",
"pt": "Desconectar do GitHub",
"es": "Desconectar de GitHub",
"ar": "قطع الاتصال من GitHub",
"fr": "Se déconnecter de GitHub",
"tr": "GitHub'dan bağlantıyı kes",
"uk": "Відключитися від GitHub"
},
"CONVERSATION$DELETE_WARNING": {
"en": "Are you sure you want to delete this conversation? This action cannot be undone.",
"ja": "この会話を削除してもよろしいですか?この操作は元に戻せません。",
+9 -1
View File
@@ -27,6 +27,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
provider_tokens_set: {},
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
condenser_max_size: DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
enable_proactive_conversation_starters:
DEFAULT_SETTINGS.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
@@ -198,7 +199,14 @@ export const handlers = [
const body = await request.json();
if (body) {
MOCK_USER_PREFERENCES.settings = MOCK_DEFAULT_USER_SETTINGS;
const current = MOCK_USER_PREFERENCES.settings || {
...MOCK_DEFAULT_USER_SETTINGS,
};
// Persist new values over current/mock defaults
MOCK_USER_PREFERENCES.settings = {
...current,
...(body as Partial<ApiSettings>),
};
return HttpResponse.json(null, { status: 200 });
}
+43
View File
@@ -48,6 +48,7 @@ function LlmSettingsScreen() {
confirmationMode: false,
enableDefaultCondenser: false,
securityAnalyzer: false,
condenserMaxSize: false,
});
// Track the currently selected model to show help text
@@ -124,6 +125,7 @@ function LlmSettingsScreen() {
confirmationMode: false,
enableDefaultCondenser: false,
securityAnalyzer: false,
condenserMaxSize: false,
});
};
@@ -181,6 +183,13 @@ function LlmSettingsScreen() {
formData.get("enable-confirmation-mode-switch")?.toString() === "on";
const enableDefaultCondenser =
formData.get("enable-memory-condenser-switch")?.toString() === "on";
const condenserMaxSizeStr = formData
.get("condenser-max-size-input")
?.toString();
const condenserMaxSize = condenserMaxSizeStr
? Number.parseInt(condenserMaxSizeStr, 10)
: undefined;
const securityAnalyzer = formData
.get("security-analyzer-input")
?.toString();
@@ -194,6 +203,8 @@ function LlmSettingsScreen() {
AGENT: agent,
CONFIRMATION_MODE: confirmationMode,
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
CONDENSER_MAX_SIZE:
condenserMaxSize ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
SECURITY_ANALYZER:
securityAnalyzer === "none"
? null
@@ -222,6 +233,7 @@ function LlmSettingsScreen() {
confirmationMode: false,
enableDefaultCondenser: false,
securityAnalyzer: false,
condenserMaxSize: false,
});
};
@@ -308,6 +320,17 @@ function LlmSettingsScreen() {
}));
};
const handleCondenserMaxSizeIsDirty = (value: string) => {
const parsed = value ? Number.parseInt(value, 10) : undefined;
const condenserMaxSizeIsDirty =
(parsed ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE) !==
(settings?.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE);
setDirtyInputs((prev) => ({
...prev,
condenserMaxSize: condenserMaxSizeIsDirty,
}));
};
const handleSecurityAnalyzerIsDirty = (securityAnalyzer: string) => {
const securityAnalyzerIsDirty =
securityAnalyzer !== settings?.SECURITY_ANALYZER;
@@ -565,6 +588,26 @@ function LlmSettingsScreen() {
/>
)}
<div className="w-full max-w-[680px]">
<SettingsInput
testId="condenser-max-size-input"
name="condenser-max-size-input"
type="number"
min={10}
step={1}
label={t(I18nKey.SETTINGS$CONDENSER_MAX_SIZE)}
defaultValue={(
settings.CONDENSER_MAX_SIZE ??
DEFAULT_SETTINGS.CONDENSER_MAX_SIZE
)?.toString()}
onChange={(value) => handleCondenserMaxSizeIsDirty(value)}
isDisabled={!settings.ENABLE_DEFAULT_CONDENSER}
/>
<p className="text-xs text-tertiary-alt mt-1">
{t(I18nKey.SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP)}
</p>
</div>
<SettingsSwitch
testId="enable-memory-condenser-switch"
name="enable-memory-condenser-switch"
+1
View File
@@ -14,6 +14,7 @@ export const DEFAULT_SETTINGS: Settings = {
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
PROVIDER_TOKENS_SET: {},
ENABLE_DEFAULT_CONDENSER: true,
CONDENSER_MAX_SIZE: 120,
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
+6 -1
View File
@@ -1 +1,6 @@
export type ConversationStatus = "STARTING" | "RUNNING" | "STOPPED";
export type ConversationStatus =
| "STARTING"
| "RUNNING"
| "STOPPED"
| "ARCHIVED"
| "ERROR";
+4
View File
@@ -47,6 +47,8 @@ export type Settings = {
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
ENABLE_DEFAULT_CONDENSER: boolean;
// Maximum number of events before the condenser runs
CONDENSER_MAX_SIZE: number | null;
ENABLE_SOUND_NOTIFICATIONS: boolean;
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
ENABLE_SOLVABILITY_ANALYSIS: boolean;
@@ -73,6 +75,8 @@ export type ApiSettings = {
security_analyzer: string | null;
remote_runtime_resource_factor: number | null;
enable_default_condenser: boolean;
// Max size for condenser in backend settings
condenser_max_size: number | null;
enable_sound_notifications: boolean;
enable_proactive_conversation_starters: boolean;
enable_solvability_analysis: boolean;
+3
View File
@@ -24,12 +24,14 @@ export const VERIFIED_MODELS = [
"kimi-k2-0711-preview",
"qwen3-coder-480b",
"gpt-5-2025-08-07",
"gpt-5-mini-2025-08-07",
];
// LiteLLM does not return OpenAI models with the provider, so we list them here to set them ourselves for consistency
// (e.g., they return `gpt-4o` instead of `openai/gpt-4o`)
export const VERIFIED_OPENAI_MODELS = [
"gpt-5-2025-08-07",
"gpt-5-mini-2025-08-07",
"gpt-4o",
"gpt-4o-mini",
"gpt-4.1",
@@ -66,6 +68,7 @@ export const VERIFIED_MISTRAL_MODELS = [
export const VERIFIED_OPENHANDS_MODELS = [
"claude-sonnet-4-20250514",
"gpt-5-2025-08-07",
"gpt-5-mini-2025-08-07",
"claude-opus-4-20250514",
"claude-opus-4-1-20250805",
"gemini-2.5-pro",
+2
View File
@@ -151,6 +151,7 @@ VERIFIED_PROVIDERS = ['openhands', 'anthropic', 'openai', 'mistral']
VERIFIED_OPENAI_MODELS = [
'gpt-5-2025-08-07',
'gpt-5-mini-2025-08-07',
'o4-mini',
'gpt-4o',
'gpt-4o-mini',
@@ -186,6 +187,7 @@ VERIFIED_MISTRAL_MODELS = [
VERIFIED_OPENHANDS_MODELS = [
'claude-sonnet-4-20250514',
'gpt-5-2025-08-07',
'gpt-5-mini-2025-08-07',
'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
'devstral-small-2507',
+3
View File
@@ -2,6 +2,8 @@ import os
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
from openhands.core.logger import openhands_logger as logger
class SandboxConfig(BaseModel):
"""Configuration for the sandbox.
@@ -55,6 +57,7 @@ class SandboxConfig(BaseModel):
)
runtime_container_image: str | None = Field(default=None)
user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000)
logger.debug(f'SandboxConfig user_id default: {user_id}')
timeout: int = Field(default=120)
remote_runtime_init_timeout: int = Field(default=180)
remote_runtime_api_timeout: int = Field(default=10)
+8 -21
View File
@@ -198,18 +198,12 @@ class ProviderHandler:
if selected_provider:
if not page or not per_page:
logger.error('Failed to provider params for paginating repos')
return []
raise ValueError('Failed to provider params for paginating repos')
service = self._get_service(selected_provider)
try:
return await service.get_paginated_repos(
page, per_page, sort, installation_id
)
except Exception as e:
logger.warning(f'Error fetching repos from {selected_provider}: {e}')
return []
return await service.get_paginated_repos(
page, per_page, sort, installation_id
)
all_repos: list[Repository] = []
for provider in self.provider_tokens:
@@ -246,17 +240,10 @@ class ProviderHandler:
if selected_provider:
service = self._get_service(selected_provider)
public = self._is_repository_url(query, selected_provider)
try:
user_repos = await service.search_repositories(
query, per_page, sort, order, public
)
return self._deduplicate_repositories(user_repos)
except Exception as e:
logger.warning(
f'Error searching repos from select provider {selected_provider}: {e}'
)
return []
user_repos = await service.search_repositories(
query, per_page, sort, order, public
)
return self._deduplicate_repositories(user_repos)
all_repos: list[Repository] = []
for provider in self.provider_tokens:
+11
View File
@@ -166,6 +166,17 @@ class LLM(RetryMixin, DebugMixin):
elif 'gemini' in self.config.model.lower() and self.config.safety_settings:
kwargs['safety_settings'] = self.config.safety_settings
# support AWS Bedrock provider
kwargs['aws_region_name'] = self.config.aws_region_name
if self.config.aws_access_key_id:
kwargs['aws_access_key_id'] = (
self.config.aws_access_key_id.get_secret_value()
)
if self.config.aws_secret_access_key:
kwargs['aws_secret_access_key'] = (
self.config.aws_secret_access_key.get_secret_value()
)
# Explicitly disable Anthropic extended thinking for Opus 4.1 to avoid
# requiring 'thinking' content blocks. See issue #10510.
if 'claude-opus-4-1' in self.config.model.lower():
+1
View File
@@ -100,6 +100,7 @@ REASONING_EFFORT_PATTERNS: list[str] = [
'gemini-2.5-pro',
'gpt-5',
'gpt-5-2025-08-07',
'gpt-5-mini-2025-08-07',
# DeepSeek reasoning family
'deepseek-r1-0528*',
]
+4 -2
View File
@@ -646,8 +646,10 @@ class ActionExecutor:
if __name__ == '__main__':
logger.warning('Starting Action Execution Server')
logger.debug('Starting Action Execution Server')
logger.debug('Arguments passed to script:')
for i, arg in enumerate(sys.argv):
logger.debug(f'Argument {i}: {arg}')
parser = argparse.ArgumentParser()
parser.add_argument('port', type=int, help='Port to listen on')
parser.add_argument('--working-dir', type=str, help='Working directory')
@@ -76,6 +76,7 @@ class RemoteRuntime(ActionExecutionClient):
user_id,
git_provider_tokens,
)
logger.debug(f'RemoteRuntime.init user_id {user_id}')
if self.config.sandbox.api_key is None:
raise ValueError(
'API key is required to use the remote runtime. '
+12 -3
View File
@@ -1,4 +1,7 @@
import traceback
from openhands.core.config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.plugins import PluginRequirement
DEFAULT_PYTHON_PREFIX = [
@@ -23,6 +26,9 @@ def get_action_execution_server_startup_command(
python_executable: str = 'python',
) -> list[str]:
sandbox_config = app_config.sandbox
logger.debug(f'app_config {vars(app_config)}')
logger.debug(f'sandbox_config {vars(sandbox_config)}')
logger.debug(f'override_user_id {override_user_id}')
# Plugin args
plugin_args = []
@@ -39,9 +45,7 @@ def get_action_execution_server_startup_command(
username = override_username or (
'openhands' if app_config.run_as_openhands else 'root'
)
user_id = override_user_id or (
sandbox_config.user_id if app_config.run_as_openhands else 0
)
user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
base_cmd = [
*python_prefix,
@@ -62,5 +66,10 @@ def get_action_execution_server_startup_command(
if not app_config.enable_browser:
base_cmd.append('--no-enable-browser')
logger.debug(f'get_action_execution_server_startup_command: {base_cmd}')
logger.debug(
'get_action_execution_server_startup_command stack:\n%s',
''.join(traceback.format_stack()),
)
return base_cmd
+55 -53
View File
@@ -49,6 +49,61 @@ def init_user_and_working_directory(
if username == os.getenv('USER') and username not in ['root', 'openhands']:
return None
# Skip root since it is already created
if username != 'root':
# Check if the username already exists
logger.debug(f'Attempting to create user `{username}` with UID {user_id}.')
existing_user_id = -1
try:
result = subprocess.run(
f'id -u {username}', shell=True, check=True, capture_output=True
)
existing_user_id = int(result.stdout.decode().strip())
# The user ID already exists, skip setup
if existing_user_id == user_id:
logger.debug(
f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
)
else:
logger.warning(
f'User `{username}` already exists with UID {existing_user_id}. Skipping user setup.'
)
return existing_user_id
return None
except subprocess.CalledProcessError as e:
# Returncode 1 indicates, that the user does not exist yet
if e.returncode == 1:
logger.debug(
f'User `{username}` does not exist. Proceeding with user creation.'
)
else:
logger.error(
f'Error checking user `{username}`, skipping setup:\n{e}\n'
)
raise
# Add sudoer
sudoer_line = r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"
output = subprocess.run(sudoer_line, shell=True, capture_output=True)
if output.returncode != 0:
raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}')
logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]')
command = (
f'useradd -rm -d /home/{username} -s /bin/bash '
f'-g root -G sudo -u {user_id} {username}'
)
output = subprocess.run(command, shell=True, capture_output=True)
if output.returncode == 0:
logger.debug(
f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
)
else:
raise RuntimeError(
f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
)
# First create the working directory, independent of the user
logger.debug(f'Client working directory: {initial_cwd}')
command = f'umask 002; mkdir -p {initial_cwd}'
@@ -64,57 +119,4 @@ def init_user_and_working_directory(
out_str += output.stdout.decode()
logger.debug(f'Created working directory. Output: [{out_str}]')
# Skip root since it is already created
if username == 'root':
return None
# Check if the username already exists
existing_user_id = -1
try:
result = subprocess.run(
f'id -u {username}', shell=True, check=True, capture_output=True
)
existing_user_id = int(result.stdout.decode().strip())
# The user ID already exists, skip setup
if existing_user_id == user_id:
logger.debug(
f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
)
else:
logger.warning(
f'User `{username}` already exists with UID {existing_user_id}. Skipping user setup.'
)
return existing_user_id
return None
except subprocess.CalledProcessError as e:
# Returncode 1 indicates, that the user does not exist yet
if e.returncode == 1:
logger.debug(
f'User `{username}` does not exist. Proceeding with user creation.'
)
else:
logger.error(f'Error checking user `{username}`, skipping setup:\n{e}\n')
raise
# Add sudoer
sudoer_line = r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"
output = subprocess.run(sudoer_line, shell=True, capture_output=True)
if output.returncode != 0:
raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}')
logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]')
command = (
f'useradd -rm -d /home/{username} -s /bin/bash '
f'-g root -G sudo -u {user_id} {username}'
)
output = subprocess.run(command, shell=True, capture_output=True)
if output.returncode == 0:
logger.debug(
f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
)
else:
raise RuntimeError(
f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
)
return None
@@ -14,12 +14,16 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
{% macro setup_base_system() %}
# Set PATH early to ensure system commands are available
ENV PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
# Install base system dependencies
{% if (('ubuntu' in base_image) or ('mswebench' in base_image)) %}
RUN apt-get update && \
apt-get install -y --no-install-recommends \
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \
coreutils util-linux procps findutils grep sed \
{%- if (base_image.endswith(':latest') or base_image.endswith(':24.04') or ('mswebench' in base_image)) -%}
libgl1 \
{%- else %}
@@ -41,6 +45,7 @@ RUN apt-get update && \
RUN apt-get update && \
apt-get install -y --no-install-recommends \
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \
coreutils util-linux procps findutils grep sed \
libgl1-mesa-glx \
libasound2-plugins libatomic1 \
# Install Docker dependencies
@@ -58,15 +63,30 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/openhands/
# Add /openhands/bin to PATH
ENV PATH="/openhands/bin:${PATH}"
# Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts
# Remove UID 1000 and GID 1000 users/groups that might conflict with openhands user
RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi)
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi) && \
(if getent group 1000 | grep -q pn; then groupdel pn; fi) && \
(if getent group 1000 | grep -q ubuntu; then groupdel ubuntu; fi)
# Create openhands group and user
RUN groupadd -g 1000 openhands && \
useradd -u 1000 -g 1000 -m -s /bin/bash openhands && \
usermod -aG sudo openhands && \
echo 'openhands ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
# Set empty password for openhands user to allow passwordless su
passwd -d openhands && \
# Set empty password for root user as well to ensure su works in both directions
passwd -d root && \
# Ensure root can su to openhands without password by configuring PAM
sed -i '/pam_rootok.so/d' /etc/pam.d/su && \
sed -i '1i auth sufficient pam_rootok.so' /etc/pam.d/su
# Create necessary directories
RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry
mkdir -p /openhands/poetry && \
chown -R openhands:openhands /openhands
# ================================================================
@@ -147,14 +167,16 @@ RUN if [ -z "${RELEASE_TAG}" ]; then \
if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi && \
mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT} && \
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz && \
chown -R openhands:openhands ${OPENVSCODE_SERVER_ROOT}
{% endmacro %}
{% macro install_vscode_extensions() %}
# Install our custom extension
# Install our custom extensions as openhands user
USER openhands
RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/hello-world/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world/
@@ -165,27 +187,72 @@ RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \
RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,ini,npm}
{% endmacro %}
{% macro install_dependencies() %}
# Install all dependencies
{% macro install_dependencies_root() %}
# Install system-level dependencies that require root
USER root
RUN \
{% if enable_browser %}
# Install system dependencies for Playwright (requires root)
apt-get update && \
apt-get install -y --no-install-recommends \
libnss3 libnspr4 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 \
libxdamage1 libxrandr2 libgbm1 libxss1 && \
# Install libasound2 - try new package name first (Ubuntu 24.04+), fallback to old name
(apt-get install -y --no-install-recommends libasound2t64 || apt-get install -y --no-install-recommends libasound2) && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
# Install Playwright browsers in shared location accessible to all users
export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers && \
mkdir -p /opt/playwright-browsers && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install chromium && \
# Set proper permissions for shared access
chmod -R 755 /opt/playwright-browsers && \
# Create cache directories and symlinks for both users
mkdir -p /home/openhands/.cache && \
mkdir -p /root/.cache && \
ln -sf /opt/playwright-browsers /home/openhands/.cache/ms-playwright && \
ln -sf /opt/playwright-browsers /root/.cache/ms-playwright && \
chown -h openhands:openhands /home/openhands/.cache/ms-playwright && \
# Set environment variable for all users
echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /etc/environment && \
{% endif %}
# Set environment variables (requires root)
/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
# Set permissions for shared read-only access
chmod -R 755 /openhands/poetry && \
chmod -R 755 /openhands/micromamba && \
chown -R openhands:openhands /openhands/poetry && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
chown -R openhands:openhands /openhands/workspace && \
chown -R openhands:openhands /openhands/micromamba && \
# Ensure PATH includes system binaries early in startup
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"' >> /etc/environment && \
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"' >> /etc/bash.bashrc && \
# Set up conda environment activation for all users
echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /etc/bash.bashrc && \
echo 'micromamba activate openhands 2>/dev/null || true' >> /etc/bash.bashrc && \
# Set up environment for root user
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/openhands/micromamba/bin:$PATH"' >> /root/.bashrc && \
echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /root/.bashrc && \
echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /root/.bashrc && \
echo 'micromamba activate openhands 2>/dev/null || true' >> /root/.bashrc && \
# Clean up system packages (requires root)
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
{% endmacro %}
{% macro install_dependencies_user() %}
# Install user-level dependencies as openhands user
WORKDIR /openhands/code
USER openhands
RUN \
/openhands/micromamba/bin/micromamba config set changeps1 False && \
/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12 && \
# Install project dependencies
/openhands/micromamba/bin/micromamba run -n openhands poetry install --only main,runtime --no-interaction --no-root && \
# Update and install additional tools
# (There used to be an "apt-get update" here, hopefully we can skip it.)
{% if enable_browser %}/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \{% endif %}
# Set environment variables
/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
# Set permissions
chmod -R g+rws /openhands/poetry && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
# Clean up
# Clean up user caches
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/micromamba/bin/micromamba clean --all
{% endmacro %}
@@ -203,7 +270,16 @@ RUN \
RUN mkdir -p /openhands/micromamba/bin && \
/bin/bash -c "PREFIX_LOCATION=/openhands/micromamba BIN_FOLDER=/openhands/micromamba/bin INIT_YES=no CONDA_FORGE_YES=yes $(curl -L https://micro.mamba.pm/install.sh)" && \
/openhands/micromamba/bin/micromamba config remove channels defaults && \
/openhands/micromamba/bin/micromamba config list
/openhands/micromamba/bin/micromamba config list && \
chown -R openhands:openhands /openhands/micromamba && \
# Create read-only shared access to micromamba for all users
# This allows both root and openhands users to access the same packages
# while maintaining security by keeping openhands as the owner
chmod -R 755 /openhands/micromamba && \
# Create a separate writable location for root's micromamba cache/config
mkdir -p /root/.local/share/micromamba && \
# Set up environment variables for system-wide access
echo 'export PATH="/openhands/micromamba/bin:$PATH"' >> /etc/environment
# Create the openhands virtual environment and install poetry and python
RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \
@@ -214,40 +290,80 @@ RUN \
if [ -d /openhands/code ]; then rm -rf /openhands/code; fi && \
mkdir -p /openhands/code/openhands && \
touch /openhands/code/openhands/__init__.py && \
chown -R openhands:openhands /openhands/code && \
# Set global git configuration to ensure proper author/committer information
git config --global user.name "openhands" && \
git config --global user.email "openhands@all-hands.dev"
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
COPY --chown=openhands:openhands ./code/pyproject.toml ./code/poetry.lock /openhands/code/
{{ install_dependencies() }}
{{ install_dependencies_user() }}
{{ install_dependencies_root() }}
# ================================================================
# END: Build Runtime Image from Scratch
# ================================================================
{% endif %}
# Ensure openhands user/group and base dirs exist even when not building from scratch
USER root
RUN \
# Remove existing user/group by name or UID/GID 1000
if getent passwd openhands >/dev/null 2>&1; then userdel -r -f openhands || true; fi && \
if getent passwd 1000 >/dev/null 2>&1; then userdel -r -f "$(getent passwd 1000 | cut -d: -f1)" || true; fi && \
if getent group openhands >/dev/null 2>&1; then groupdel openhands || true; fi && \
if getent group 1000 >/dev/null 2>&1; then groupdel "$(getent group 1000 | cut -d: -f1)" || true; fi && \
\
# Recreate group with GID 1000
groupadd -g 1000 openhands && \
\
# Recreate user with UID 1000
useradd -u 1000 -g openhands -m -s /bin/bash openhands && \
\
# Ensure home and required directories exist
mkdir -p /home/openhands && \
mkdir -p /openhands && \
mkdir -p $(dirname ${OPENVSCODE_SERVER_ROOT}) && \
\
# Ensure ownership is correct
chown -R openhands:openhands /home/openhands || true && \
chown -R openhands:openhands /openhands || true
{{ setup_vscode_server() }}
# ================================================================
# Copy Project source files
# ================================================================
RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
COPY --chown=openhands:openhands ./code/pyproject.toml ./code/poetry.lock /openhands/code/
RUN if [ -d /openhands/code/microagents ]; then rm -rf /openhands/code/microagents; fi
COPY ./code/microagents /openhands/code/microagents
COPY ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py
COPY --chown=openhands:openhands ./code/microagents /openhands/code/microagents
COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py && \
chown -R openhands:openhands /openhands/code
# ================================================================
# END: Build from versioned image
# ================================================================
{% if build_from_versioned %}
{{ install_dependencies() }}
{{ install_dependencies_user() }}
{{ install_dependencies_root() }}
{{ install_vscode_extensions() }}
{% endif %}
# Install extra dependencies if specified
{% if extra_deps %}RUN {{ extra_deps }} {% endif %}
# Install extra dependencies if specified (as openhands user)
{% if extra_deps %}
USER openhands
RUN {{ extra_deps }}
{% endif %}
# Set up environment for openhands user
USER root
RUN \
# Set up environment for openhands user
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/openhands/micromamba/bin:$PATH"' >> /home/openhands/.bashrc && \
echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /home/openhands/.bashrc && \
echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /home/openhands/.bashrc && \
echo 'micromamba activate openhands 2>/dev/null || true' >> /home/openhands/.bashrc && \
chown openhands:openhands /home/openhands/.bashrc
+5 -2
View File
@@ -203,12 +203,15 @@ class Session:
# The order matters: with the browser output first, the summarizer
# will only see the most recent browser output, which should keep
# the summarization cost down.
max_events_for_condenser = settings.condenser_max_size or 120
default_condenser_config = CondenserPipelineConfig(
condensers=[
ConversationWindowCondenserConfig(),
BrowserOutputCondenserConfig(attention_window=2),
LLMSummarizingCondenserConfig(
llm_config=llm_config, keep_first=4, max_size=120
llm_config=llm_config,
keep_first=4,
max_size=max_events_for_condenser,
),
]
)
@@ -218,7 +221,7 @@ class Session:
f' browser_output_masking(attention_window=2), '
f' llm(model="{llm_config.model}", '
f' base_url="{llm_config.base_url}", '
f' keep_first=4, max_size=80)'
f' keep_first=4, max_size={max_events_for_condenser})'
)
agent_config.condenser = default_condenser_config
agent = Agent.get_cls(agent_cls)(agent_config, self.llm_registry)
@@ -1,7 +1,23 @@
"""
This class is similar to the RuntimeStatus defined in the runtime api. (When this class was defined
a RuntimeStatus class already existed in OpenHands which serves a completely different purpose) Some of
the status definitions do not match up:
STOPPED/paused - the runtime is not running but may be restarted
ARCHIVED/stopped - the runtime is not running and will not restart due to deleted files.
"""
from enum import Enum
class ConversationStatus(Enum):
# The conversation is starting
STARTING = 'STARTING'
# The conversation is running - the agent may be working or idle
RUNNING = 'RUNNING'
# The conversation has stopped (This is synonymous with `paused` in the runtime API.)
STOPPED = 'STOPPED'
# The conversation has been archived and cannot be restarted.
ARCHIVED = 'ARCHIVED'
# Something has gone wrong with the conversation (The runtime rather than the agent)
ERROR = 'ERROR'
+12
View File
@@ -7,6 +7,7 @@ from pydantic import (
SecretStr,
SerializationInfo,
field_serializer,
field_validator,
model_validator,
)
from pydantic.json import pydantic_encoder
@@ -42,6 +43,8 @@ class Settings(BaseModel):
search_api_key: SecretStr | None = None
sandbox_api_key: SecretStr | None = None
max_budget_per_task: float | None = None
# Maximum number of events in the conversation view before condensation runs
condenser_max_size: int | None = None
email: str | None = None
email_verified: bool | None = None
git_user_name: str | None = None
@@ -102,6 +105,15 @@ class Settings(BaseModel):
data['secret_store'] = secret_store
return data
@field_validator('condenser_max_size')
@classmethod
def validate_condenser_max_size(cls, v: int | None) -> int | None:
if v is None:
return v
if v < 10:
raise ValueError('condenser_max_size must be at least 10')
return v
@field_serializer('secrets_store')
def secrets_store_serializer(self, secrets: UserSecrets, info: SerializationInfo):
"""Custom serializer for secrets store."""
+1
View File
@@ -57,6 +57,7 @@ def get_supported_llm_models(config: OpenHandsConfig) -> list[str]:
openhands_models = [
'openhands/claude-sonnet-4-20250514',
'openhands/gpt-5-2025-08-07',
'openhands/gpt-5-mini-2025-08-07',
'openhands/claude-opus-4-20250514',
'openhands/gemini-2.5-pro',
'openhands/o3',
+12
View File
@@ -86,6 +86,15 @@ def test_model_matches_provider_qualified(name, pattern, expected):
supports_stop_words=True,
),
),
(
'gpt-5-mini-2025-08-07',
ModelFeatures(
supports_function_calling=True,
supports_reasoning_effort=True,
supports_prompt_cache=False,
supports_stop_words=True,
),
),
(
'o3-mini',
ModelFeatures(
@@ -172,6 +181,7 @@ def test_get_features(model, expect):
'gpt-4o',
'gpt-4.1',
'gpt-5',
'gpt-5-mini-2025-08-07',
# o-series
'o1-2024-12-17',
'o3-mini',
@@ -199,6 +209,7 @@ def test_function_calling_models(model):
'gemini-2.5-flash',
'gemini-2.5-pro',
'gpt-5',
'gpt-5-mini-2025-08-07',
],
)
def test_reasoning_effort_models(model):
@@ -252,6 +263,7 @@ def test_prompt_cache_models(model):
('gemini-2.5-pro', True),
('gpt-5', True),
('gpt-5-2025-08-07', True),
('gpt-5-mini-2025-08-07', True),
('claude-opus-4-1-20250805', False),
# DeepSeek
('deepseek/DeepSeek-R1-0528:671b-Q4_K_XL', True),
@@ -166,7 +166,10 @@ def test_generate_dockerfile_build_from_scratch():
assert 'python=3.12' in dockerfile_content
# Check the update command
assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content
assert (
'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands'
in dockerfile_content
)
assert (
'/openhands/micromamba/bin/micromamba run -n openhands poetry install'
in dockerfile_content
@@ -188,7 +191,10 @@ def test_generate_dockerfile_build_from_lock():
assert 'poetry install' not in dockerfile_content
# These update commands SHOULD still in the dockerfile
assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content
assert (
'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands'
in dockerfile_content
)
def test_generate_dockerfile_build_from_versioned():
@@ -206,7 +212,10 @@ def test_generate_dockerfile_build_from_versioned():
# this SHOULD exist when build from versioned
assert 'poetry install' in dockerfile_content
assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content
assert (
'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands'
in dockerfile_content
)
def test_get_runtime_image_repo_and_tag_eventstream():