Compare commits

..

630 Commits

Author SHA1 Message Date
HeyItsChloe
d21b04affe FE: Restore flag to block traffic to /onboarding (#14166) 2026-04-27 20:37:45 -04:00
Hiep Le
fc0b69dbf8 fix: enforce onboarding completion on every navigation (#14142)
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-04-27 20:37:40 -04:00
Graham Neubig
82492f1769 fix: normalize legacy MCP config in migration 108 (#14116)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-24 13:12:16 -04:00
Graham Neubig
50907e1500 fix: preserve LLM and MCP settings in migration 108 (#14112)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-24 10:40:20 -04:00
Hiep Le
4ee63d5bd2 fix(frontend): show members a read-only badge on org-defaults pages (#14098) 2026-04-23 12:57:19 -04:00
Graham Neubig
97b173979e Fix enterprise migration 108 settings mapping (#14088)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-23 12:51:03 -04:00
Rohit Malhotra
033c6202b7 fix: handle nullable datetime in _fix_timezone methods (#14073)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 19:16:26 +00:00
Graham Neubig
d64d0d6bf6 Hide All toggle on SaaS LLM settings (#14013)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-04-22 15:13:57 -04:00
aivong-openhands
b357c0c3bb Fix CVE-2026-39892: Update cryptography to 46.0.7 (#13968)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-04-22 21:07:29 +02:00
Rohit Malhotra
16374dc9c0 fix: add ColumnElement type annotation for SQLAlchemy filter conditions (#14072)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 19:06:08 +00:00
Graham Neubig
a8926068ff fix: restore org settings payload contract (#14051)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-22 15:03:40 -04:00
chuckbutkus
f318792a17 security: Invalidate SESSION_API_KEY on pause and require RUNNING status (#14001)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-04-22 12:49:28 -06:00
Rohit Malhotra
505095d50a fix: migrate core SQLAlchemy models to SQLAlchemy 2.0 mapped_column (#14065)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 17:54:08 +00:00
Rohit Malhotra
51f9266abb fix(enterprise): migrate StoredVerifiedModel to SQLAlchemy 2.0 mapped_column (#14064)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 17:53:48 +00:00
Rohit Malhotra
439fa8fc30 fix(enterprise): migrate storage models to SQLAlchemy 2.0 [13/13] (#13859)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 10:41:38 -07:00
Rohit Malhotra
c1ae41acb9 fix(enterprise): migrate org-related models to SQLAlchemy 2.0 [12/13] (#13858)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 17:16:29 +00:00
Rohit Malhotra
270d9b1cce fix(enterprise): migrate Slack models to SQLAlchemy 2.0 [8/13] (#13854)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 12:57:38 -04:00
Rohit Malhotra
3b0e201a4e fix(enterprise): migrate Jira models to SQLAlchemy 2.0 [9/13] (#13855)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 12:57:20 -04:00
Rohit Malhotra
cd24b5838b fix(enterprise): migrate Git provider models to SQLAlchemy 2.0 [11/13] (#13857)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 12:53:53 -04:00
Rohit Malhotra
1509018ee2 fix(enterprise): migrate Linear models to SQLAlchemy 2.0 [10/13] (#13856)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 12:53:45 -04:00
Tim O'Farrell
1605e97d80 APP-1374 Removed V0 sessions (#14061)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 08:47:17 -06:00
Xingyao Wang
06d0320e5c fix(frontend): restore notification sound and browser tab flash on agent completion (#14049)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-22 15:22:33 +01:00
Graham Neubig
f7dce9c6c0 Allow supported Python interpreters in Makefile setup (#14012)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2026-04-22 08:25:56 -04:00
Tim O'Farrell
13e9d7584a Remove openhands.server.conversation_manager package (V0 to V1 migration) (#14034)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 20:17:11 -06:00
Tim O'Farrell
e0a4c35c9c APP-1362 Remove V0 linter Package (#14055) 2026-04-21 20:05:20 -06:00
Tim O'Farrell
701231cbf3 APP-1361 Remove V0 security Package (#14054) 2026-04-21 20:05:10 -06:00
simonrosenberg
f8a43f9937 chore: use OPENHANDS_BOT_GITHUB_PAT_PUBLIC (#14041)
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-04-21 20:19:21 -03:00
Tim O'Farrell
c49ed64b64 APP-1366 Removed V0 Critic (#14056) 2026-04-21 15:35:30 -06:00
aivong-openhands
3b17f27dee PLTF-139: Add Analytics redirect URI to Keycloak allhands client (#14052)
Co-authored-by: Test Admin <ai.vong+bdctestadmin@openhands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 15:14:44 -05:00
Rohit Malhotra
ae2f13ecba fix(enterprise): migrate auth models to SQLAlchemy 2.0 [7/13] (#13853)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 15:07:12 -04:00
Rohit Malhotra
6d1850e94b fix(enterprise): migrate user models to SQLAlchemy 2.0 [6/13] (#13852)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 14:49:19 -04:00
chuckbutkus
cf7e88c8c3 security: Require RUNNING status for session API key validation (#14002)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 18:48:22 +00:00
Rohit Malhotra
6420f1cd7c fix(enterprise): migrate conversation_callback model to SQLAlchemy 2.0 [5/13] (#13851)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 14:38:18 -04:00
Rohit Malhotra
c7de3dfc91 fix(enterprise): migrate org models to SQLAlchemy 2.0 [4/13] (#13850)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 14:38:10 -04:00
Rohit Malhotra
393a6bb8f8 feat: add automation event forwarding for GitHub webhooks (#13793)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@openhands.dev>
Co-authored-by: chuckbutkus <chuck@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-21 13:41:53 -04:00
Hiep Le
d8c67a4d3d fix(backend): repair org-defaults LLM save flow and sync managed keys to members (#14044) 2026-04-22 00:38:52 +07:00
Rohit Malhotra
237e9f530e feat: pass WEB_HOST to agent context in V1 conversations (#14050)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 12:12:14 -04:00
OpenHands Bot
93ae8aae43 Regenerate poetry.lock with Poetry 2.3.3 after rebase (#13911)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 16:29:15 +01:00
Xingyao Wang
595bb4749d fix: invalidate sandbox and VS Code URL caches on conversation resume (#13988)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-21 16:18:01 +01:00
Joe Laverty
b43d9b1929 feat: Tag images instead of rebuilding on git tags; stop publishing v0 runtime image (#14005)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2026-04-21 14:58:04 +00:00
Xingyao Wang
3fa9b84aa4 fix(frontend): Fix ThinkAction rendering showing raw JSON in chat (#13981)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-04-21 15:57:46 +01:00
dependabot[bot]
db8ab2715e chore(deps): bump the security-all group across 1 directory with 3 updates (#13932)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 16:40:51 +02:00
Graham Neubig
fa0da8f3bd fix: prevent infinite redirect loop on org-defaults settings pages (#14042)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-20 18:55:49 -04:00
Hiep Le
0da1f70b91 fix(frontend): show org-wide settings badge beside title on org-defaults pages (#14031) 2026-04-21 02:18:17 +07:00
Hiep Le
3892ab2b67 fix(frontend): hide and block personal LLM/Condenser/Verification settings in team orgs (#14030) 2026-04-21 00:52:26 +07:00
Rohit Malhotra
30dc1655b1 fix(enterprise): migrate telemetry models to SQLAlchemy 2.0 [3/13] (#13849)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-20 13:00:42 -04:00
Graham Neubig
71ce61acd2 Remove agent from the LLM settings page (#14033)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-20 12:16:01 -04:00
Hiep Le
b2df428eff fix(backend): restore git-organizations endpoint for git conversation routing (#14032) 2026-04-20 22:48:24 +07:00
Graham Neubig
7bbef99771 Move Tavily search key to MCP settings (#14000)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-04-20 13:45:02 +00:00
Tim O'Farrell
fd014e8e23 Removing the deprecated agenthub package (#14024)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-19 21:42:22 -06:00
OpenHands Bot
89f3dceeb8 fix(security): redact session_api_key from WebSocket access logs (#14019)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-19 16:54:31 +00:00
OpenHands Bot
dcb6ac3599 fix(security): redact API keys from MCP config logging (#14020)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-19 13:53:40 -03:00
Tim O'Farrell
3b264dd419 Remove deprecated V0 FastAPI endpoints (#13952)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-19 08:41:16 -06:00
dependabot[bot]
f212e0e856 chore(deps): bump node from 25.8-trixie-slim to 25.9-trixie-slim in /containers/app (#13829)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-18 20:29:14 +02:00
dependabot[bot]
918b0a8b59 chore(deps-dev): bump pytest from 9.0.2 to 9.0.3 (#13935)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-18 20:29:11 +02:00
dependabot[bot]
119b0c99a8 chore(deps): bump pygments from 2.19.2 to 2.20.0 (#13934)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-18 20:14:59 +02:00
dependabot[bot]
0628679307 chore(deps): bump docker/build-push-action from 6 to 7 (#13961)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 15:50:00 +02:00
xsf
e8249f00a8 docs: align AgentHub task tracking references (#13708)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2026-04-18 15:40:14 +02:00
dependabot[bot]
1651edf8c9 chore(deps): bump docker/login-action from 3 to 4 (#13960)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 15:34:16 +02:00
dependabot[bot]
1fd94675d0 chore(deps): bump actions/download-artifact from 7 to 8 (#13700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2026-04-18 15:34:13 +02:00
dependabot[bot]
b841e1acb0 chore(deps): bump docker/metadata-action from 5 to 6 (#13959)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 15:34:10 +02:00
JL2001
1af04f2833 docs: fix app_server README to reflect actual module structure (#13890) 2026-04-18 15:26:24 +02:00
aivong-openhands
b87f08f651 docs: add note about updating enterprise poetry lockfile (#13761)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-18 15:24:56 +02:00
aivong-openhands
e23af62a57 PLTF-1269: add docstrings to mock classes in maintenance runner tests (#13931)
Co-authored-by: Test Admin <ai.vong+bdctestadmin@openhands.dev>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-18 15:23:45 +02:00
Graham Neubig
9db83a1555 Refresh git settings after provider updates (#13979)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-18 07:47:44 -04:00
Octopus
8f5b3ceb6c fix(settings): align MCP settings layout with other settings pages (#13996)
Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2026-04-18 11:19:48 +00:00
buyua9
5bb9e4a567 docs: fix broken React Router test doc links (#13802) 2026-04-17 22:47:27 +00:00
dependabot[bot]
a5a7a86600 chore(deps): bump actions/github-script from 7 to 9 (#13958)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:06:40 +02:00
aivong-openhands
5c8d7c4c2d Fix CVE-2026-40347: Update python-multipart to 0.0.26 (#13965)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-04-17 12:32:15 -05:00
Xingyao Wang
2068694ea0 fix(enterprise): Apply deployment LLM proxy URL override in /api/v1/users/me (#13980)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-17 12:23:42 +07:00
Tim O'Farrell
385122e260 refactor: Replace load_custom_secrets_names with search_custom_secrets (#13985)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-16 18:50:38 -06:00
Ash Clarke
97343ebe9a Fix V1 resumed conversation status sync (#13989)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-16 15:49:44 -06:00
Joe Laverty
926f25a74b bugfix(enterprise): Remove shared conversation enumerator endpoints (#13976) 2026-04-16 18:11:44 +00:00
Tim O'Farrell
52c4d0d9d9 Fix get_latest_token to return str instead of SecretStr (#13974)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-16 11:41:43 -06:00
Hiep Le
f1ff98b2fc fix: add flat SDK compat fields to /api/v1/users/me response (#13957) 2026-04-16 23:11:51 +07:00
dependabot[bot]
26c43d1955 chore(deps): bump pillow from 12.1.1 to 12.2.0 (#13933)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 10:47:35 -05:00
Graham Neubig
d81c2bc0a6 Fix duplicate enterprise migration revision (#13950)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-16 11:02:08 -04:00
Tim O'Farrell
fdf5c398fd frontend: Remove deprecated V0 conversation APIs (#13963)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-16 08:57:25 -06:00
HeyItsChloe
c78b923468 feat(frontend): Remove onboarding feature flags (#13947)
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-16 12:28:27 +07:00
Tim O'Farrell
db78925d77 Fix merge error in migrations (#13951) 2026-04-16 00:40:19 +00:00
Graham Neubig
b4da0e1c69 settings: expose SDK settings schema to OpenHands (#13306)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: tofarr <tofarr@gmail.com>
2026-04-15 17:00:35 -06:00
Vasco Schiavo
d548665bcf feat(frontend): add /btw side-channel command for asking the agent mid-task (#13918)
Co-authored-by: Juan Michelini <juan@juan.com.uy>
2026-04-15 18:47:44 -03:00
Engel Nyst
eb940ea5e7 Use SDK package public exports where available (#13402)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-15 22:05:08 +02:00
HeyItsChloe
22b91976fd feat(frontend): removed PROJ_USER_JOURNEY feature flag (#13940)
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-16 01:35:19 +07:00
Hiep Le
dcf044f8c3 feat(frontend): add automations button to sidebar (#13941) 2026-04-16 01:34:55 +07:00
OpenHands Bot
d58106b29b fix(security): extend RedactURLParamsFilter to cover all uvicorn log formats (#13914)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: simonrosenberg <157206163+simonrosenberg@users.noreply.github.com>
2026-04-15 15:24:24 -03:00
OpenHands Bot
e11faa6dd1 fix(security): redact MCP config secrets before logging (#13913)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: simonrosenberg <157206163+simonrosenberg@users.noreply.github.com>
2026-04-15 15:22:50 -03:00
HeyItsChloe
b4b77fbc31 feat: Add DEPLOYMENT_MODE detection for conditional onboarding (#13675)
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-16 00:57:57 +07:00
Tim O'Farrell
ef452b6544 Fix concurrent GET /api/v1/web-client/config calls (#13905)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-14 15:02:45 -06:00
Tim O'Farrell
0eafa9fd15 APP-1170: Jira : add V1 conversation support and multiple bug fixes (#13909)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-14 14:39:00 -06:00
Joe Laverty
ab64a65f25 feat(enterprise): Build ARM64 image for enterprise, use native runners for all images (#13921) 2026-04-14 16:28:18 -04:00
Engel Nyst
4cdf88d480 Update LICENSE (#13924) 2026-04-14 19:03:41 +00:00
Xingyao Wang
eab9d9e3c7 fix: paginate through backend pages when filtering shared events (#13910)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-14 14:20:33 +01:00
Hiep Le
58df84e16c feat: add manage automations permission and broadcast org selection (#13908) 2026-04-14 18:05:02 +07:00
Vasco Schiavo
3cd74d3bac fix(jira): use markdown_to_jira_markup for proper wiki markup rendering (#13879)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-13 15:40:01 -06:00
simonrosenberg
20018842a4 fix(security): suppress SQLAlchemy DEBUG logging that leaks credentials (#13719)
Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:49:25 -03:00
Ricardo-M-L
cce2080ae0 fix: correct wrong variable in max_size validation error message in condensers (#13816) 2026-04-13 09:44:41 -06:00
Ricardo-M-L
a0304b9e4c fix: correct logger format args and -0 slice bug (#13817) 2026-04-13 09:43:30 -06:00
Tim O'Farrell
de492b792f refactor(llm-settings): Use V1 providers/models endpoints, lazy-load models per provider (#13892)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-12 22:42:57 -06:00
gpothier
7a6eb7e07c fix: fallback to LLM_BASE_URL for openhands_provider_base_url (#13880)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-11 16:49:13 -06:00
dependabot[bot]
c92178ac6b chore(deps): bump docker/metadata-action from 5 to 6 (#13702)
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-12 00:48:47 +02:00
dependabot[bot]
5400fea1e4 chore(deps): bump actions/upload-artifact from 5 to 7 (#13699)
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-12 00:47:48 +02:00
dependabot[bot]
635b090065 chore(deps): bump actions/checkout from 4 to 6 (#13703)
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-12 00:46:37 +02:00
dependabot[bot]
f3815a769f chore(deps): bump docker/login-action from 3 to 4 (#13701)
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-12 00:45:13 +02:00
Graham Neubig
4f81d2ae7a Filter out ConversationStateUpdateEvent from shared-events endpoints (#13888)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-11 16:03:02 -05:00
Tim O'Farrell
a06b9ccffa Remove dead frontend exports (#13891)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-11 12:13:04 -06:00
Tim O'Farrell
8406dcb82f Remove dead frontend exports (#13889)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-11 11:40:12 -06:00
Tim O'Farrell
6c0a92c2cd Update frontend to use V1 settings endpoints (#13887)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-11 09:27:53 -06:00
Tim O'Farrell
7f25348506 Migrate git-service API to V1 endpoints (#13877)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-11 08:11:58 -06:00
aivong-openhands
e9067237f2 Fix CVE-2025-64340: Update fastmcp to 3.2.0 (#13685)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 20:08:57 +00:00
Tim O'Farrell
cae7d36522 Remove unused startConversation method and dead code (#13876)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 13:24:42 -06:00
Tim O'Farrell
27a2d59c23 Update getUser() to use V1 API endpoint /api/v1/users/git-info (#13875)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 12:23:00 -06:00
Tim O'Farrell
d3d916745a Update Suggestions Service API to use new V1 endpoint with pagination (#13872)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 11:36:15 -06:00
Tim O'Farrell
50f1d332cc Remove V1 enabled flag and agents from frontend (#13871)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 11:14:25 -06:00
Tim O'Farrell
de53245d1b refactor(frontend): Remove unused API methods from conversation-service.api.ts (#13870)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 09:43:33 -06:00
Vasco Schiavo
8c2661638e fix(slack): use markdown_text parameter for proper Markdown rendering in V1 (#13869)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 11:37:20 -04:00
Tim O'Farrell
bdbaba0c34 Remove unused searchEventsV0 method from EventService (#13865)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 09:20:58 -06:00
Tim O'Farrell
d866d735d9 refactor(frontend): Remove V0 conversation creation path (#13823)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 06:58:51 -06:00
Tim O'Farrell
39f3b293f5 Fix: Use container StartedAt for Docker sandbox status grace period calculation (#13841)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 06:58:26 -06:00
Rohit Malhotra
fa4afa9412 fix(enterprise): migrate device_code model to SQLAlchemy 2.0 [2/13] (#13848)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 05:13:31 +00:00
Rohit Malhotra
f274d5e90f fix(enterprise): migrate simple storage models to SQLAlchemy 2.0 [1/13] (#13847)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 01:04:05 -04:00
Rohit Malhotra
dd5eb69c65 fix(enterprise): enable SQLAlchemy 2.0 type checking foundation (#13846)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-10 00:42:17 -04:00
OpenHands Bot
21d86b6b5e fix: redact MCP server secrets from log output (#13840)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: simonrosenberg <157206163+simonrosenberg@users.noreply.github.com>
2026-04-09 19:02:28 -03:00
OpenHands Bot
2c2e37902f fix: redact session_api_key from uvicorn WebSocket access logs (#13839)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: simonrosenberg <157206163+simonrosenberg@users.noreply.github.com>
2026-04-09 19:02:23 -03:00
Tim O'Farrell
f7f029ec1a Removed the path for creating V0 conversations in the API. (#13837)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-09 15:10:27 -06:00
Graham Neubig
3e9017bb6e Remove CODEOWNERS file (#13833)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-09 16:55:58 -04:00
Tim O'Farrell
78e48ace2d Remove microagent management UI (#13835)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-09 13:18:24 -06:00
chuckbutkus
60ece6d7c2 feat: Add organization/authorization info to /api/v1/users/me endpoint (#13822)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-04-09 14:37:13 -04:00
Vasco Schiavo
738e7a9834 feat(frontend): render GFM tables with visible borders in chat messages (#13825) 2026-04-09 16:16:13 +07:00
aivong-openhands
8b4a1f9763 Fix CVE-2026-34591: Update poetry to 2.3.3 (#13711)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-09 00:07:42 +02:00
Tim O'Farrell
0804abec80 Remove V0-only feedback functionality (#13821)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-08 13:48:41 -06:00
Tim O'Farrell
06c3d9c17b Remove microagent functionality from frontend code (#13820)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-08 12:19:44 -06:00
Tim O'Farrell
754a96e7f3 chore(frontend): remove unused hooks and code (#13810)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-07 13:10:19 -06:00
Tim O'Farrell
211b73a088 Refactor conversation list to use V1 API (#13803)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-07 12:35:11 -06:00
Hiep Le
54041dd093 feat: remove ENABLE_ORG_CLAIMS_RESOLVER_ROUTING feature flag (#13809) 2026-04-08 00:55:36 +07:00
Hiep Le
f271346724 feat(backend): route Jira resolver conversations to claimed org workspaces (#13805) 2026-04-07 23:58:52 +07:00
Hiep Le
d6a0dd7fe4 feat(backend): route Linear resolver conversations to claimed org workspaces (#13804) 2026-04-07 23:22:48 +07:00
Tim O'Farrell
e46bcfa82f Add V1 API endpoints for git search and branches (#13794)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-07 06:52:56 -06:00
Tim O'Farrell
2eefa5edfd Deprecate /api/options/models, add /api/v1/config/models/search endpoint (#13799)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-07 06:51:49 -06:00
Ray Myers
54858c0fc0 ci: retire Blacksmith from all GitHub Actions workflows (#13795)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-06 16:51:09 -05:00
Rohit Malhotra
384c324652 fix(slack): immediately display 'No Repository' option (#13791)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-06 14:21:59 -04:00
Tim O'Farrell
4e68f57807 Add V1 git routes with pagination for installations and repositories (#13790)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-06 12:01:22 -06:00
Jamie Chicago
649ebc4078 Succinct pr template (#13779)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-06 19:05:24 +02:00
Tim O'Farrell
e3246c27d4 Added new v1 endpoint for user git info and deprecated old endpoint (#13787)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-06 09:54:24 -06:00
Ray Myers
72194f19db chore: Add sdk to mypy checking and fix the resulting errors (#13637)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2026-04-06 11:43:31 -04:00
gpothier
0c5e30ab33 Add KVM device passthrough support for hardware virtualization (#13618)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-04-06 14:57:58 +00:00
simonrosenberg
b8f2932b02 fix(security): redact credentials from MCP config logging (#13720)
Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-06 08:46:42 -06:00
dependabot[bot]
62673c028a chore(deps): bump the version-all group across 1 directory with 7 updates (#13774)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: tofarr <tofarr@gmail.com>
2026-04-06 08:39:09 -06:00
Hiep Le
7af2285fe6 fix(backend): custom API key overwritten when using non-OpenHands provider in basic view (#13785) 2026-04-06 21:14:14 +07:00
Hiep Le
69d281c6be fix(frontend): prevent budget/credit error banner from disappearing immediately (#13786) 2026-04-06 21:13:30 +07:00
Jamie Chicago
8ce3089a68 Add contributors section to README (#13696)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-04 01:27:45 +02:00
Tim O'Farrell
b9b10ebf5e APP-1197 Mark conversation endpoints as deprecated with updated docs (#13775)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-03 14:45:32 -06:00
Tim O'Farrell
ce6d5b77c4 Add more endpoints as deprecated (microagent repository endpoints) (#13776)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-03 20:45:14 +00:00
simonrosenberg
a458c9b785 Fix credential leak in callback event logging (#13718)
Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:29:26 +00:00
Hiep Le
a65ddc3db6 feat(backend): route Slack resolver conversations to claimed org workspaces (#13758) 2026-04-04 03:09:21 +07:00
Tim O'Farrell
732a1c1991 APP-1197 Migrate secrets endpoints to V1 API (#13770)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-03 14:06:51 -06:00
Hiep Le
d058323a87 feat(backend): route gitlab resolver conversations to claimed org workspaces (#13755) 2026-04-04 02:27:46 +07:00
aivong-openhands
7d04cffe4e Fix CVE-2026-25645: Update requests to 2.33.1 (#13692)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-04-03 13:55:31 -05:00
Hiep Le
6ad27b77bb feat(backend): route resolver conversations to claimed org workspaces (#13713) 2026-04-04 01:32:43 +07:00
aivong-openhands
2739fc8fbe Fix CVE-2026-22815: Update aiohttp to 3.13.5 (#13705)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-04-03 13:21:05 -05:00
dependabot[bot]
38b7e10252 chore(deps): bump the security-all group across 1 directory with 2 updates (#13764)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 11:46:46 -05:00
mamoodi
7b7d1c0c55 Update CODEOWNERS (#13762) 2026-04-03 12:01:58 -04:00
Tim O'Farrell
e38eda4ac9 APP-1197 Migrate settings endpoints to V1 API (/api/v1/settings) (#13759)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-03 09:38:24 -06:00
aivong-openhands
99c19b6ef0 enterprise lock update openhands aci to version already in openhands (#13704)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-03 09:57:14 -04:00
Jathin Sreenivas
0731e8c68a feat(frontend): Display LLM model on conversation cards and header (#13616)
Co-authored-by: Jathin Sreenivas <sjathin@amazon.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-04-03 17:57:37 +07:00
Tim O'Farrell
0a9570eea2 APP-1197 Consolidate health routes to app_server package (#13724)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-04-02 21:34:40 -06:00
Rohit Malhotra
c00f90bf86 feat: add tags storage for conversation metadata (#13680)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-03 00:54:27 +00:00
aivong-openhands
1bbf699498 Add Laminar redirect URI to Keycloak allhands client (#13666)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-02 14:15:59 -05:00
Rohit Malhotra
f76517732d Add git to app container runtime dependencies (#13715)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-02 14:43:23 -04:00
Hiep Le
7bb567734d feat(frontend): replace mocked git conversation routing with real API integration (#13698) 2026-04-03 01:05:28 +07:00
aivong-openhands
45f0c77f36 Fix CVE-2026-33699: Update pypdf to 6.9.2 (#13689)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-04-02 11:14:39 -05:00
dependabot[bot]
fe3d33f222 chore(deps): bump the security-all group across 1 directory with 2 updates (#13706)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 10:57:05 -05:00
dependabot[bot]
2b53d44c2a chore(deps): bump the security-all group across 1 directory with 1 update (#13607)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-02 10:32:36 -04:00
dependabot[bot]
0541cb58b2 chore(deps): bump dawidd6/action-download-artifact from 6 to 15 (#13001)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-04-02 09:55:12 -04:00
Hiep Le
5d593ca6e4 feat(backend): add API endpoints to claim and disconnect git organizations (#13683) 2026-04-02 12:35:30 +07:00
Jamie Chicago
2158e30e87 Fix README intro link formatting (#13695)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-02 02:32:01 +02:00
aivong-openhands
7b4ae66e5a fix: upgrade pip to fix CVE-2025-8869 (#13640)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-04-01 16:53:11 -05:00
Graham Neubig
3e1e8f00f7 refactor: single source of truth for verified models (#13421)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Juan Michelini <juan@juan.com.uy>
2026-04-01 18:00:29 -03:00
Joe Laverty
74a69b2dcc ci: add cloud-semver tag support for enterprise image (#13687) 2026-04-01 14:50:15 -04:00
mamoodi
fc36913518 ci: skip PyPI release for cloud- tags (#13686)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-01 13:18:51 -04:00
Engel Nyst
c788674b41 fix: remove resolver summary language hint (#13684)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-01 16:35:28 +02:00
dependabot[bot]
849548a132 chore(deps): bump actions/stale from 9 to 10 (#12261)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2026-03-31 16:34:21 -04:00
dependabot[bot]
c73e22d7cd chore(deps): bump actions/download-artifact from 6 to 7 (#12260)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-03-31 16:25:58 -04:00
dependabot[bot]
6304f9f4c5 chore(deps): bump actions/checkout from 4 to 6 (#12259)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-03-31 16:25:24 -04:00
dependabot[bot]
93be4d9d0b chore(deps): bump peter-evans/find-comment from 3 to 4 (#12190)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-03-31 16:23:51 -04:00
Hiep Le
ec66250e74 feat(backend): develop api to retrieve git organizations for the current organization (#13676) 2026-04-01 01:31:14 +07:00
Engel Nyst
dbd199e77c Validate selected branch names before checkout (#13667)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-31 18:21:21 +02:00
Jamie Chicago
f0c454caf1 Improve README trusted-by logos across light and dark themes (#13659)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-31 12:18:24 -04:00
Hiep Le
df3360005c feat(frontend): add Git Conversation Routing section for org claims UI (#13668) 2026-03-31 22:14:45 +07:00
Jamie Chicago
df4fea6aca Revert "[fix] maintainer doc" (#13673) 2026-03-31 11:09:58 -04:00
Hiep Le
2b3868ddc3 feat(frontend): add feature flag for organization claims resolver routing (#13669) 2026-03-31 21:39:36 +07:00
Joe Laverty
e3c9fa9d05 Remove unused KEYCLOAK_PROVIDER_NAME constant (#13663)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-31 09:46:25 -04:00
Hiep Le
2fec71320a fix(frontend): pin axios version to mitigate supply chain attack (#13670) 2026-03-31 19:29:02 +07:00
Hiep Le
9c0f5d785e fix(backend): persist disabled_skills in SaaS settings store (#13658) 2026-03-31 02:23:08 +07:00
Tim O'Farrell
73ba66faea Handling the new server error event (#13643)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-30 10:56:37 -06:00
aivong-openhands
a198599d91 docs(AGENTS.md): add guidance to preserve tool versions when regenerating lockfiles (#13561)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-30 12:23:39 -04:00
mamoodi
7e20bd51f9 Release 1.6.0 (#13604)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-30 12:01:16 -04:00
Hiep Le
b75c83d92a fix(frontend): prevent duplicate payment successful toast after Stripe checkout (#13649) 2026-03-30 22:36:35 +07:00
Hiep Le
5528b01c18 refactor(frontend): replace loading spinner with static icon for task tracking (#13625) 2026-03-30 20:32:11 +07:00
Hiep Le
ed5ab11fcc fix: planning agent auth error due to missing base_url (#13638) 2026-03-30 20:32:02 +07:00
Hiep Le
e1afc95b6c fix(frontend): hide right panel when active tab is unpinned (#13648) 2026-03-30 20:31:48 +07:00
Tim O'Farrell
6dd9046ba2 Fix issue where git setup fails on remote sandboxed when grouping. (#13646)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-30 12:58:42 +00:00
Xingyao Wang
9ad47bf43f fix: prevent V0 conversation creation due to settings race condition (#13628)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-30 13:11:25 +01:00
Jathin Sreenivas
b0d8244ad5 fix(frontend): prevent "Unknown event" shown for actions with empty d… (#13639)
Co-authored-by: Jathin Sreenivas <sjathin@amazon.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-30 16:49:25 +07:00
Karanja
c210d5294f feat: add /new to slash command menu for V1 conversations (#13599) 2026-03-30 15:39:35 +07:00
Tim O'Farrell
c7190ddb30 APP-1153 Fix for issue where popup menu does not display (#13635)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-28 07:47:13 -06:00
Hiep Le
df64ce9668 fix(frontend): reduce padding and gap for chat status indicator (#13624) 2026-03-28 01:39:02 +07:00
Jamie Chicago
f72a9622f6 [fix] maintainer doc (#13632)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-27 14:33:48 -04:00
Tim O'Farrell
193eb34dc7 fix(migration): serialize dict to JSON string in migration 103 (#13634)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-27 18:22:31 +00:00
Hiep Le
87f582db6a fix(frontend): tab icon overflow on mobile devices (#13627) 2026-03-28 00:25:39 +07:00
Hiep Le
4b69370c73 fix(frontend): set max width for toast messages (#13623) 2026-03-28 00:25:26 +07:00
Hiep Le
74ac6e06a1 refactor(frontend): add white background color on learn more button hover (user journey project) (#13621) 2026-03-28 00:25:12 +07:00
Hiep Le
a91dceacfb fix(frontend): add missing border radius to diff view (#13620) 2026-03-28 00:25:01 +07:00
Joe Laverty
98c61e1ee4 feat(enterprise): acquire pg_advisory_lock before running database migrations (#13608) 2026-03-27 23:24:49 +07:00
Tim O'Farrell
3268c29945 APP-1152 Add legacy fallback variable when finding persistence directory (#13629) 2026-03-27 10:18:13 -06:00
Engel Nyst
239e40da75 Fix: restore conversation link in PR bodies created via MCP (#13092)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-27 11:25:34 -04:00
Jamie Chicago
d190d8ee50 Add trusted-by logos to top of README (#13613)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-27 15:32:39 +01:00
aivong-openhands
5f064fa88b PLTF-330: log module funcName and lineno in enterprise (#13612)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-27 09:18:02 -05:00
Vasco Schiavo
8f87ef59c7 feat(frontend): Add view mode toggle (old/diff/new) to file changes viewer (#13519)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-27 19:16:20 +07:00
Vasco Schiavo
fdc6ba82c9 feat(frontend): Display skill ready events as expandable skill list in chat (#13511)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-27 18:57:47 +07:00
Hiep Le
a75038bee0 fix: user does not immediately appear in org after accepting invite in openhands cloud (#13562) 2026-03-27 14:37:38 +07:00
Hiep Le
fbe6eb30cb feat(backend): add organization members financial data endpoint (#13595) 2026-03-27 12:18:46 +07:00
Hiep Le
aeda0ea762 feat(frontend): display toast notification when switching organizations (#13598) 2026-03-27 12:18:17 +07:00
Hiep Le
30b7af31b9 feat(frontend): add contextual info messages on LLM settings page (org project) (#13601) 2026-03-27 12:17:58 +07:00
Hiep Le
05a3916c98 feat(frontend): use LoginCTA in device verify with source-specific Learn more behavior (#13606) 2026-03-27 12:17:38 +07:00
Tim O'Farrell
eba1f60c1d Reduced thrash on sandbox service (#13610)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-26 15:29:59 -06:00
OpenHands Bot
024f4d3326 Bump SDK packages to v1.15.0 (#13602)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
2026-03-26 14:34:17 -06:00
Ray Myers
3e38f13d12 perf: speed up Docker builds — amd64-only PRs, eliminate cross-layer chmod/chown bloat (#13590)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-03-26 11:57:31 -06:00
Tim O'Farrell
8a61fc824b Fix for issue where messages is null and error occurs (#13592)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-26 08:02:46 -06:00
Hiep Le
6794603963 feat(frontend): update settings UI with section headers and dividers (org project) (#13584) 2026-03-26 12:37:53 +07:00
Hiep Le
9be60bc286 fix: make MCP settings user-specific within organization (#13591) 2026-03-26 11:42:08 +07:00
Xingyao Wang
f7b53283b5 fix(frontend): guard against undefined matcher.hooks in hooks modal (#13589)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-25 18:20:46 +00:00
Tim O'Farrell
3cd85a07b7 APP-1093 fix(frontend): display 'Starting' status when server reports STARTING on conversation resume (#13580)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-25 08:55:39 -04:00
Hiep Le
0b935669f3 fix(backend): clean up orphaned Keycloak users on duplicate email rejection (#13495) 2026-03-25 16:46:20 +07:00
Hiep Le
889754abfd fix: use API key's org_id when creating conversations via API key auth (#13568) 2026-03-25 16:46:06 +07:00
Tim O'Farrell
06cd53d752 APP-1113 fix: Increase polling time for SetTitleCallbackProcessor (#13577)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-24 17:40:40 -06:00
Tim O'Farrell
eb189144f2 APP-1115 Fix for AWS config (Minio) for feature branches (#13579)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-24 17:40:04 -06:00
statxc
c9b2ce2fb9 feat: add user-configurable enable/disable of default global skills w… (#13046)
Co-authored-by: intelliking <intelliking@users.noreply.github.com>
2026-03-24 14:48:22 -06:00
HeyItsChloe
abdc58cd28 feat(frontend): lead capture form (#13496)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-24 13:41:35 -07:00
aivong-openhands
9f47727da5 PLTF-330: add timestamp to enterprise JSON logger formatter (#13555)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-24 14:53:14 -05:00
Ash Clarke
19da63aae6 Log all terminal states (error, stuck) in V1 callback processors (#13549)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-24 13:04:39 -05:00
Rohit Malhotra
f1b65d9534 Rename env name (#13570) 2026-03-24 16:38:49 +00:00
aivong-openhands
3516c3cdbe chore(deps): make pythonnet Windows-only dependency (#13515)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-24 11:21:25 -05:00
Tim O'Farrell
1f275a7cfe fix: reuse db session in migrate_customer call causing FK violation (#13558)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 21:10:45 -06:00
Tim O'Farrell
ff240c968b fix: add 30s timeout to LiteLlmManager HTTP client (#13557)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 21:43:02 +00:00
aivong-openhands
36039d2bb8 upgrade setuptools in /enterprise for updated wheel CVE-2026-24049 (#13509)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 16:37:20 -05:00
Tim O'Farrell
45529fa451 Added Falsy check for base url (#13553) 2026-03-23 13:06:25 -06:00
Tim O'Farrell
0fc4b0fb55 Add infinite scroll pagination and filesystem storage support to public share page (#13545)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 12:18:07 -06:00
Tim O'Farrell
810fc340fc Fix count endpoint 500 error (#13548)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 17:40:56 +00:00
Tim O'Farrell
33a0f95dac Small typo fix (#13546) 2026-03-23 15:36:17 +00:00
aivong-openhands
bdd0214266 chore: increase dependabot open-pull-requests-limit to 5 (#13538)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 10:28:32 -05:00
Saurya Velagapudi
7fbb499f03 feat: switch default base image to nikolaik slim variant (#13244)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 10:26:04 -05:00
aivong-openhands
abbfbda450 chore(frontend): update flatted to 3.4.2 (#13503)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-23 10:26:30 -04:00
John-Mason P. Shackelford
7774f43ca1 feat(frontend): Add /launch route for starting conversations with plugins (#12699)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-03-23 15:06:42 +07:00
Vasco Schiavo
b705b015fa fix(frontend): rounded corners on diff viewer bottom in Changes tab (#13521) 2026-03-23 14:06:23 +07:00
Jathin Sreenivas
1581b95ab9 fix(frontend): Ensure error and status messages wrap correctly within containers (#13522)
Co-authored-by: Jathin Sreenivas <sjathin@amazon.com>
2026-03-23 13:55:49 +07:00
aivong-openhands
94b45c6c36 PLTF-327: upgrade enterprise nodejs to v24 LTS (#13507)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-20 14:42:03 -05:00
dependabot[bot]
cbc380fe49 chore(deps): bump node from 25.2-trixie-slim to 25.8-trixie-slim in /containers/app (#13316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-03-20 14:40:23 -05:00
Vasco Schiavo
fb776ef650 feat(frontend): Add copy button to code blocks (#13458)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-20 18:20:25 +07:00
Abi
a75b576f1c fix: treat llm_base_url="" as explicit clear in store_llm_settings (#13471)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 11:14:15 +01:00
Rohit Malhotra
63956c3292 Fix FastAPI Query parameter validation: lte -> le (#13502)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-19 20:27:10 -04:00
chuckbutkus
f75141af3e fix: prevent secrets deletion across organizations when storing secrets (#13500)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-19 19:34:12 -04:00
dependabot[bot]
e4515b21eb chore(deps): bump socket.io-parser from 4.2.5 to 4.2.6 in /frontend in the security-all group across 1 directory (#13474)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 17:28:15 -04:00
aivong-openhands
a8f6a35341 fix: patch GLib CVE-2025-14087 in runtime Docker images (#13403)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-19 16:21:24 -05:00
Joe Laverty
f706a217d0 fix: Use commit SHA instead of mutable branch tag for enterprise base (#13498) 2026-03-19 16:24:07 -04:00
aivong-openhands
0137201903 fix: remove vulnerable VSCode extensions in build_from_scratch path (#13399)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2026-03-19 19:36:22 +00:00
aivong-openhands
49a98885ab chore: Update OpenSSL in Debian images for security patches (#13401)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-19 14:33:23 -05:00
Hiep Le
38648bddb3 fix(frontend): use correct git path based on sandbox grouping strategy (#13488) 2026-03-20 00:13:02 +07:00
Hiep Le
b44774d2be refactor(frontend): extract AddCreditsModal into separate component file (#13490) 2026-03-20 00:12:48 +07:00
Hiep Le
04330898b6 refactor(frontend): add delay before closing user context menu (#13491) 2026-03-20 00:12:38 +07:00
Chris Bagwell
120fd7516a Fix: Prevent auto-logout on 401 errors in oss mode (#13466) 2026-03-19 16:33:01 +01:00
chuckbutkus
2224127ac3 Fix when budgets are None (#13482)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-19 10:14:48 -05:00
aivong-openhands
2d1e9fa35b Fix CVE-2026-33123: Update pypdf to 6.9.1 (#13473)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-19 11:05:30 -04:00
MkDev11
0ec962e96b feat: add /clear endpoint for V1 conversations (#12786)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-19 21:13:58 +07:00
Engel Nyst
3a9f00aa37 Keep VSCode accessible when agent errors (#13492)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-19 14:46:56 +01:00
Hiep Le
e02dbb8974 fix(backend): validate API key org_id during authorization to prevent cross-org access (org project) (#13468) 2026-03-19 16:09:37 +07:00
Hiep Le
8039807c3f fix(frontend): scope organization data queries by organization ID (org project) (#13459) 2026-03-19 14:18:29 +07:00
Saurya Velagapudi
a96760eea7 fix: ensure LiteLLM user exists before generating API keys (#12667)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 17:16:43 -07:00
Saurya Velagapudi
dcb2e21b87 feat: Auto-forward LLM_* env vars to agent-server and fix host network config (#13192)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 17:07:19 -07:00
Tim O'Farrell
7edebcbc0c fix: use atomic write in LocalFileStore to prevent race conditions (#13480)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-18 16:49:32 -06:00
HeyItsChloe
abd1f9948f fix: return empty skills list instead of 404 for stopped sandboxes (#13429)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 14:46:00 -06:00
aivong-openhands
2879e58781 Fix CVE-2026-30922: Update pyasn1 to 0.6.3 (#13452)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-18 16:00:06 -04:00
Rohit Malhotra
1d1ffc2be0 feat(enterprise): Add service API for automation API key creation (#13467)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 19:07:36 +00:00
Hiep Le
db41148396 feat(backend): expose API key org_id via new GET /api/keys/current endpoint (org project) (#13469) 2026-03-19 01:46:23 +07:00
Robert Brennan
39a4ca422f fix: use sentence case for 'Waiting for sandbox' text (#12958)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 14:42:46 -04:00
Varun Chawla
6d86803f41 Add loading feedback to git changes refresh button (#12792)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-19 01:26:27 +07:00
Jordi Mas
8e0386c416 feat: add Catalan translation (#13299) 2026-03-18 13:17:43 -04:00
Nelson Spence
48cd85e47e fix(security): add sleep to container wait loop (#12869)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 13:04:36 -04:00
不做了睡大觉
c62b47dcb1 fix: handle empty body in GitHub issue resolver (#13039)
Co-authored-by: User <user@example.com>
2026-03-18 12:36:52 -04:00
Jamie Chicago
eb9a822d4c Update CONTRIBUTING.md (#13463) 2026-03-18 12:10:22 -04:00
Engel Nyst
fb7333aa62 fix: stop calling agent-server /generate_title (#13093)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 17:10:07 +01:00
aivong-openhands
fb23418803 clarify docstring for provider token reference (#13386)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 12:03:56 -04:00
Xingyao Wang
991585c05d docs: add cross-repo testing skill for SDK ↔ OH Cloud e2e workflow (#13446)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 16:00:23 +00:00
Chris Bagwell
35a40ddee8 fix: handle containers with tagless images in DockerSandboxService (#13238) 2026-03-18 11:55:48 -04:00
Hiep Le
5d1f9f815a fix(frontend): preserve settings page route on browser refresh (org project) (#13462) 2026-03-18 22:50:42 +07:00
Hiep Le
d3bf989e77 feat(frontend): improve conversation access error message with workspace hint (org project) (#13461) 2026-03-18 22:50:30 +07:00
Hiep Le
6589e592e3 feat(frontend): add contextual info messages on LLM settings page (org project) (#13460) 2026-03-18 22:50:16 +07:00
Chris Bagwell
fe4c0569f7 Remove unused WORK_HOSTS_SKILL_FOOTER (#12594) 2026-03-18 21:57:23 +07:00
Xingyao Wang
28ecf06404 Render V1 paired tool summaries (#13451)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 10:52:05 +00:00
dependabot[bot]
26fa1185a4 chore(deps): bump mcp from 1.25.0 to 1.26.0 in the mcp-packages group (#13314)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-03-17 17:44:35 -05:00
HeyItsChloe
d3a8b037f2 feat(frontend): home page cta (#13339)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-18 03:44:36 +07:00
HeyItsChloe
af1fa8961a feat(frontend): login page cta (#13337)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 03:14:59 +07:00
HeyItsChloe
3b215c4ad1 feat(frontend): context menu cta (#13338)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-18 02:52:02 +07:00
HeyItsChloe
7516b53f5a feat(frontend): self hosted new user questions (#13367)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-18 02:51:40 +07:00
aivong-openhands
855ef7ba5f PLTF-309: disable budget enforcement when ENABLE_BILLING=false (#13440)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-17 14:26:13 -05:00
Rohit Malhotra
09ca1b882f (Hotfix): use direct attrib for file download result (#13448) 2026-03-17 14:48:46 -04:00
Jamie Chicago
79cfffce60 docs: Improve Development.md and CONTRIBUTING.md with OS-specific setup guides (#13432)
Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-17 17:03:33 +01:00
Saurya Velagapudi
b68c75252d Add architecture diagrams explaining system components and WebSocket flow (#12542)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Saurya <saurya@openhands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2026-03-17 08:52:40 -07:00
aivong-openhands
d58e12ad74 Fix CVE-2026-27962: Update authlib to 1.6.9 (#13439)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-17 10:13:08 -05:00
Engel Nyst
bd837039dd chore: update skills path comments (#12794) 2026-03-17 10:45:50 -04:00
Kooltek68
8a7779068a docs: fix typo in README.md (#13444) 2026-03-17 10:16:31 -04:00
Neha Prasad
38099934b6 fix : planner PLAN.md rendering and search labels (#13418)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-17 20:59:02 +07:00
Xingyao Wang
75c823c486 feat: expose_secrets param on /users/me + sandbox-scoped secrets API (#13383)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-17 12:54:57 +00:00
Tim O'Farrell
8941111c4e refactor: use status instead of pod_status in RemoteSandboxService (#13436)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-16 17:34:27 -06:00
ankit kumar
59dd1da7d6 fix: update deprecated libtmux API calls (#12596)
Co-authored-by: ANKIT <ankit@ANKITs-MacBook-Air.local>
2026-03-16 18:21:05 -04:00
Rohit Malhotra
934fbe93c2 Feat: enterprise banner option during device oauth (#13361)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-16 18:54:36 +00:00
Xingyao Wang
55e4f07200 fix: add missing params to TestLoadHooksFromWorkspace setup (#13424)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-16 14:49:36 -04:00
Xingyao Wang
00daaa41d3 feat: Load workspace hooks for V1 conversations and add hooks viewer UI (#12773)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: Alona King <alona@all-hands.dev>
2026-03-17 00:55:23 +08:00
HeyItsChloe
a0e777503e fix(frontend): prevent auto sandbox resume behavior (#13133)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-16 10:22:23 -06:00
Hiep Le
238cab4d08 fix(frontend): prevent chat message loss during websocket disconnections or page refresh (#13380) 2026-03-16 22:25:44 +07:00
Tim O'Farrell
aec95ecf3b feat(frontend): update stop sandbox dialog to display conversations in sandbox (#13388)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-16 05:20:10 -06:00
Tim O'Farrell
d591b140c8 feat: Add configurable sandbox reuse with grouping strategies (#11922)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-16 05:19:31 -06:00
Rohit Malhotra
4dfcd68153 (Hotfix): followup messages for slack conversations (#13411)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-15 14:23:06 -04:00
aivong-openhands
f7ca32126f Fix CVE-2026-32597: Update pyjwt to 2.12.0 (#13405)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-14 09:35:56 -05:00
Hiep Le
c66a112bf5 fix(frontend): add rendering support for GlobObservation and GrepObservation events (#13379) 2026-03-14 19:56:57 +07:00
Ray Myers
a8ff720b40 chore: Update imagemagick in Debian images for security patches (#13397) 2026-03-13 22:48:50 -05:00
chuckbutkus
a14158e818 fix: use query params for file upload path (#13376)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 21:08:23 -04:00
John-Mason P. Shackelford
0c51089ab6 Upgrade the SDK to 1.14.0 (#13398)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 21:07:20 -04:00
chuckbutkus
8189d21445 Fix async call to await return (#13395) 2026-03-13 19:13:18 -04:00
chuckbutkus
b7e5c9d25b Use a flag to indicate if new users should use V1 (#13393)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 22:39:07 +00:00
chuckbutkus
873dc6628f Add Enterprise SSO login button to V1 login page (#13390)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 16:57:34 -04:00
chuckbutkus
f5d0af15d9 Add default initial budget for teams/users (#13389)
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 16:57:03 -04:00
chuckbutkus
922e3a2431 Add AwsSharedEventService for shared conversations (#13141)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 14:32:58 -04:00
Tim O'Farrell
0527c46bba Add sandbox_id__eq filter to AppConversationService search and count methods (#13387)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 11:24:58 -06:00
Hiep Le
b4f00379b8 fix(frontend): auto-scroll not working in Planner tab when plan content updates (#13355) 2026-03-13 23:47:03 +07:00
sp.wack
cd2d0ee9a5 feat(frontend): Organizational support (#9496)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: Abhay Mishra <grabhaymishra@gmail.com>
Co-authored-by: Hyun Han <62870362+smosco@users.noreply.github.com>
Co-authored-by: Nhan Nguyen <nhan13574@gmail.com>
Co-authored-by: Bharath A V <avbharath1221@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: Chloe <chloe@openhands.com>
Co-authored-by: HeyItsChloe <54480367+HeyItsChloe@users.noreply.github.com>
2026-03-13 23:38:54 +07:00
Tim O'Farrell
8e6d05fc3a Add sandbox_id__eq filter parameter to search/count conversation methods (#13385)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 15:30:16 +00:00
Hiep Le
9d82f97a82 fix(frontend): address the responsive issue on the integrations page (#13354) 2026-03-13 21:28:38 +07:00
Hiep Le
2c7b25ab1c fix(frontend): address the responsive issue on the home page (#13353) 2026-03-13 21:28:15 +07:00
aivong-openhands
e82bf44324 Fix CVE-2025-67221: Update orjson to 3.11.6+ (#13371)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-13 06:58:56 -05:00
Xingyao Wang
8799c07027 fix: add PR creation instructions to V1 issue comment template and fix summary prompt (#13377)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-13 14:35:22 +08:00
Tim O'Farrell
8b8ed5be96 fix: Revert on_conversation_update to load conversation inside method (#13368)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-12 19:08:04 -06:00
Tim O'Farrell
c1328f512d Upgrade the SDK to 1.13.0 (#13365) 2026-03-12 13:28:19 -06:00
Tim O'Farrell
e2805dea75 Fix pagination bug in event_service_base.search_events causing duplicate events in exports (#13364)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-12 12:24:06 -06:00
aivong-openhands
127e611706 Fix GHSA-78cv-mqj4-43f7: Update tornado to 6.5.5 (#13362)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-12 13:22:39 -05:00
Hiep Le
a176a135da fix: sdk conversations not appearing in cloud ui (#13296) 2026-03-12 22:23:08 +07:00
Tim O'Farrell
ab78d7d6e8 fix: Set correct user context in webhook callbacks based on sandbox owner (#13340)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-12 09:11:35 -06:00
mamoodi
4eb6e4da09 Release 1.5.0 (#13336) 2026-03-11 14:50:13 -04:00
dependabot[bot]
7e66304746 chore(deps): bump pypdf from 6.7.5 to 6.8.0 (#13348)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-11 12:09:09 -05:00
Graham Neubig
a8b12e8eb8 Remove Common Room sync scripts (#13347)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-11 10:48:37 -04:00
Xingyao Wang
53bb82fe2e fix: use project_dir consistently for workspace.working_dir, setup.sh, and git hooks (#13329)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-11 15:26:34 +08:00
Tim O'Farrell
db40eb1e94 Using the web_url where it is configured rather than the request.url (#13319)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-10 13:11:33 -06:00
Hiep Le
debbaae385 fix(backend): inherit organization llm settings for new members (#13330) 2026-03-11 01:28:46 +07:00
Juan Michelini
5e5950b091 Add Gemini-3.1-Pro-Preview model support to frontend (#13253)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2026-03-10 16:18:13 +00:00
John-Mason P. Shackelford
c7ff560465 Fix getGitPath to handle nested GitLab group paths (#13006)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-10 11:12:08 -05:00
Joe Laverty
3432bbbb88 fix: Remove N+1 request from Bitbucket Data Center integration (#13281) 2026-03-10 11:08:30 -05:00
Hiep Le
fc24be2627 fix(frontend): preserve login_method param to enable session re-authentication (#13310) 2026-03-10 22:52:40 +07:00
Hiep Le
bc72b38d6e fix(backend): propagate LLM settings to all org members when admin saves settings (#13326) 2026-03-10 22:52:01 +07:00
Dream
145f1266e6 feat(frontend): create a separate UI tab for monitoring tasks (#13065)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-10 22:31:38 +07:00
Rohit Malhotra
e12dd924ce feat(slack): implement repo selection with external_select for pagination (#13273)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-09 16:04:49 -04:00
Tim O'Farrell
598b381e3d Added fallback for sandbox spec service (#13317)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-09 12:21:52 -06:00
Ray Myers
698cfc2520 fix: sanitize file_path in git diff shell commands to prevent command injection (#13051)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-09 17:29:25 +00:00
Xingyao Wang
8356170193 Fix stale Docker image tags & add version consistency CI + update-sdk skill (#13315)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-09 17:23:30 +00:00
mamoodi
fe2e50fc7d Use tag version instead of commit for agent server image (#13312) 2026-03-09 10:46:21 -04:00
aivong-openhands
ef840b046a remove mcp version check for cve_2025_66416 (#13277) 2026-03-09 09:38:44 -05:00
Tim O'Farrell
c8fe39b176 Upgrading SDK to 1.12.0 (#13248) 2026-03-09 21:06:12 +07:00
Ray Myers
8c46df6b59 fix: asyncpg, device key timestamp without timezone, error reporting (#13301)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-08 12:56:59 -05:00
Engel Nyst
b37adbc1e6 Remove deprecated reset-settings endpoint (#13298)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-08 13:11:35 +01:00
Tim O'Farrell
3ec999e88a Fix LiteLLM key management and user migration SQL queries (#13279)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-06 21:48:19 -07:00
Jamie Chicago
d1c2185d99 [fix] update welcome email to new cloud sign ups (#13254)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-06 15:24:55 -06:00
Joe Laverty
ede203add3 feat(enterprise): Bitbucket Data Center Integration (#13228)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-06 10:49:20 -06:00
aivong-openhands
b0cdd0358f fix: add mcp>=1.25 constraint and CVE-2025-66416 tests (#13247)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-06 10:31:46 -06:00
Tim O'Farrell
6186685ebc Refactor user authorization: Replace domain blocklist with flexible whitelist/blacklist pattern matching (#13207)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-06 09:10:59 -07:00
jpelletier1
2d7362bf26 refactor: update skills to Agent Skills format (#13267)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-06 09:22:28 -05:00
Povo43
1f1fb5a954 fix(i18n): correct Japanese translation strings (#13261) 2026-03-06 14:15:27 +04:00
Chris Bagwell
41d8bd28e9 fix: preserve llm_base_url when saving MCP server config (#13225) 2026-03-06 02:39:58 +01:00
Rohit Malhotra
6c394cc415 Add rate limiting to verification emails during OAuth flow (#13255)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-05 19:10:25 -05:00
Rohit Malhotra
4c380e5a58 feat: Add timeout handling for Slack repo query (#13249)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-06 00:02:04 +00:00
Chris Bagwell
ded0363e36 fix: ensure VSCode tab popout works for V1 (#13118) 2026-03-06 00:53:15 +01:00
Juan Michelini
d8444ef626 Add Qwen3-Coder-Next model support to frontend (#13222)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-05 19:34:06 -03:00
Juan Michelini
64e96b7c3c Add Kimi-K2.5 model support to frontend (#13227)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-05 19:33:59 -03:00
aivong-openhands
dcef5ae1f1 Fix CVE-2026-0540: Override dompurify to version 3.3.2 (#13230)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-05 14:42:20 -06:00
aivong-openhands
cfbf29f6e8 chore: downgrade fastmcp to 2.12.4 in uv.lock (#13240)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-05 14:42:01 -06:00
sp.wack
59b369047f Revert "Fix dark background in chat status row hiding message content" (#13239) 2026-03-05 19:40:06 +04:00
Hiep Le
07468e39f7 feat(frontend): disable the create a plan button when users are using the planning agent (#13234) 2026-03-05 22:19:50 +07:00
Hiep Le
0b0bfdff05 feat(frontend): add the build button to the planner tab (#13235) 2026-03-05 22:19:14 +07:00
aivong-openhands
42b0a89366 Fix CVE-2026-28802: Update authlib to 1.6.7 (#13229)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-05 08:56:47 -06:00
layla
e78d7de0c0 Fix dark background in chat status row hiding message content (#13236) 2026-03-05 18:47:11 +04:00
Juan Michelini
6751bba939 Add GLM-5 model support to frontend (#13213)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-05 18:44:33 +04:00
aivong-openhands
039e966dad update mcp to 1.25.0 in uv lock (#13231) 2026-03-05 07:42:24 -06:00
Juan Michelini
a1f73bb4c6 Add Claude-Sonnet-4-6 model support to frontend (#13224)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 21:13:30 -03:00
Rohit Malhotra
bf769d1744 Handle deleted GitHub issues (410 error) gracefully (#13217)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 15:50:37 -05:00
Tim O'Farrell
15e9435b35 Remove ExperimentManager concept from codebase (#13215)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 13:41:18 -07:00
Rohit Malhotra
3e15b849a3 Downgrade solvability disabled log from warning to info (#13216)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 15:26:21 -05:00
Rohit Malhotra
c32934ed2f Improve budget exceeded error handling in V1 callback processors (#13219)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 15:25:46 -05:00
Hiep Le
518fb2ee24 fix(frontend): hide add team members button when anonymous analytics is disabled (#13209) 2026-03-05 02:03:47 +07:00
Hiep Le
eeac9f14a3 fix(backend): allow deleting an organization after recent changes (#13200) 2026-03-05 01:52:33 +07:00
Rohit Malhotra
039e208167 Fix enterprise mypy type checking to catch type mismatches (#13140)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 13:43:46 -05:00
Hiep Le
6f8bf24226 feat: hide the users, billing, and integration pages for self-hosted customers (#13199) 2026-03-05 01:24:06 +07:00
Rohit Malhotra
6e9e906946 Remove dead test code for non-existent update_common_room_signal function (#13211)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 13:20:25 -05:00
Rohit Malhotra
30245dedef Fix mypy type errors in enterprise/integrations/github/github_manager.py (#13208)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 13:19:48 -05:00
Rohit Malhotra
3bf019b045 Fix mypy type errors in enterprise/server/auth and clustered_conversation_manager (#13210)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 18:19:01 +00:00
Rohit Malhotra
ab02c73c7c Fix mypy type errors in enterprise/storage/ (#13204)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 13:17:21 -05:00
Rohit Malhotra
b8db9ecd53 Fix mypy type errors in enterprise GitLab integration (#13205)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 16:13:16 +00:00
Rohit Malhotra
b86b2f16af Fix mypy type errors in enterprise/server/routes/billing.py and api_keys.py (#13196)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 11:06:26 -05:00
Juan Michelini
a11435b061 Add GLM-4.7 model support to frontend (#13202)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 15:40:15 +00:00
Engel Nyst
f01c8dd955 V1 resolver: move PR/issue context into initial user message (#12983)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 10:21:08 -05:00
Rohit Malhotra
baae3780e5 Fix mypy type errors in enterprise/integrations/github/ (#13191)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 10:04:36 -05:00
Rohit Malhotra
1fb28604e6 Fix mypy type errors in server/services and storage/org_service (#13190)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-04 10:04:27 -05:00
Tim O'Farrell
8dac1095d7 Refactor user_store.py to use async database sessions (#13187)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 17:51:53 -07:00
Rohit Malhotra
222e8bd03d Fix linear-related mypy type errors and make Manager.start_job async (#13189)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 19:00:53 -05:00
aivong-openhands
0ae9128ed7 Fix CVE-2025-69223: Update aiohttp to 3.13.3 (#13008)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-03-03 17:43:05 -06:00
Tim O'Farrell
4fc5351ed7 Refactor openhands_pr_store.py to use async db sessions (#13186)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 16:38:41 -07:00
Rohit Malhotra
a1271dc129 Fix mypy type errors in token_manager.py and auth_token_store.py (#13179)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-03 23:37:38 +00:00
Tim O'Farrell
45b970c0dd Refactor RoleStore to fully async (#13184)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 16:01:20 -07:00
Rohit Malhotra
4688741324 Fix Slack integration mypy type errors (#13177)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 17:44:20 -05:00
Rohit Malhotra
79a0cee7d9 Fix mypy type errors in Jira integration (#13181)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 17:43:44 -05:00
mamoodi
d19ba0d166 Clarify upcoming-release skill (#13185) 2026-03-03 16:01:32 -06:00
mamoodi
63654c4643 Add a new upcoming-release skill (#13180) 2026-03-03 16:15:55 -05:00
Tim O'Farrell
2f11f6a39a refactor(enterprise): Convert OrgMemberStore to fully async (#13173)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 20:26:07 +00:00
Rohit Malhotra
5cad59a661 Fix UserData validation error when GitHub user has no OpenHands account (#13135)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 20:22:57 +00:00
Rohit Malhotra
6dff07ea35 Fix union-attr mypy errors in enterprise code (#13176)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 20:22:54 +00:00
mamoodi
117ea0466d Add script that outputs the PRs between two commits (#13175) 2026-03-03 15:18:55 -05:00
Rohit Malhotra
6822169594 Fix type signatures for mypy compliance + V1 GitLab Support (#13171)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 20:03:45 +00:00
dependabot[bot]
35024aeffe chore(deps): bump pypdf from 6.7.3 to 6.7.5 (#13157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-03-03 12:45:43 -06:00
Rohit Malhotra
a051f7d6f6 Add generic type hints to manager class declarations (#13174)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 12:19:12 -05:00
Rohit Malhotra
4fe3da498a Fix GitLab integration type errors for mypy compliance (#13172)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 12:19:05 -05:00
Rohit Malhotra
b890e53a6e Fix UserAuth vs SaasUserAuth type annotations (#13149)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 11:14:10 -05:00
aivong-openhands
8aa730105a Fix CVE-2023-36464 update openhands-aci in enterprise poetry lock to remove pypdf2 (#13170) 2026-03-03 10:01:43 -06:00
Pierrick Hymbert
e7934ea6e5 feat(bitbucket): supports cloud and server APIs (#11052)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: Chris Bagwell <chris@cnpbagwell.com>
Co-authored-by: CHANGE <joe.laverty@openhands.dev>
Co-authored-by: Joe Laverty <jlav@users.noreply.github.com>
2026-03-03 10:51:43 -05:00
aivong-openhands
a927b9dc73 Fix CVE-2023-36464 update to openhands-aci 0.3.3 to remove pypdf2 (#13142) 2026-03-03 09:19:50 -06:00
aivong-openhands
0b9fd442bd chore: update uv lock and enterprise poetry lock to replace python-jose with jwcrypto (#13105)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 09:19:30 -06:00
Tim O'Farrell
501bf64312 Make SlackTeamStore fully async (#13160)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 07:07:44 -07:00
Hiep Le
6f1a7ddadd fix(backend): resolve timezone mismatch in validate_api_key causing database error (#13158) 2026-03-03 20:54:10 +07:00
Tim O'Farrell
f3026583d7 Refactor enterprise code to use async database sessions (Round 3) (#13148)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 06:35:19 -07:00
Tim O'Farrell
4a3a42c858 refactor(enterprise): make OrgStore fully async (#13154)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-03-03 10:47:22 +00:00
Hiep Le
2d057bb7b4 fix(backend): resolve timezone mismatch in accept_tos causing database error (#13155) 2026-03-03 17:08:11 +07:00
bendarte
a7a4eb2664 fix(ui): sync pin/unpin state across conversation tabs (#12884) (#12932)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-03 16:33:28 +07:00
chuckbutkus
0c7ce4ad48 V1 Changes to Support Path Based Routing (#13120)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 22:37:37 -05:00
Rohit Malhotra
4dab34e7b0 fix(enterprise): fix type errors - missing returns and async interface (#13145)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 00:37:22 +00:00
Rohit Malhotra
f8bbd352a9 Fix typing: make Message a dict instead of dict | str (#13144)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-03 00:30:22 +00:00
Tim O'Farrell
17347a95f8 Make load_org_token and store_org_token async in TokenManager (#13147)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 17:08:21 -07:00
Graham Neubig
01ef87aaaa Add logging when sandbox is assigned to conversation (#13143)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 17:36:49 -05:00
Hiep Le
8059c18b57 fix(backend): update planning agent to direct users to the build button instead of asking ready to proceed (#13139) 2026-03-03 03:31:29 +07:00
Tim O'Farrell
c82ee4c7db refactor(enterprise): use async database sessions in feedback routes (#13137)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 13:17:44 -07:00
Tim O'Farrell
7fdb423f99 feat(enterprise): convert DeviceCodeStore to async (#13136)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 12:56:41 -07:00
dependabot[bot]
530065dfa7 chore(deps): bump pillow from 12.1.0 to 12.1.1 in uv lock and enterprise poetry lock (#13101)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 13:56:13 -06:00
Hiep Le
a4cd2d81a5 fix(backend): use run_coroutine_threadsafe for conversation update callbacks (#13134) 2026-03-03 02:07:32 +07:00
Tim O'Farrell
003b430e96 Refactor: Migrate remaining enterprise modules to async database sessions (#13124)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 11:52:00 -07:00
Graham Neubig
d63565186e Add Claude Opus 4.6 model support (#12767)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: neubig <neubig@users.noreply.github.com>
2026-03-02 13:12:48 -05:00
Hiep Le
5f42d03ec5 fix(backend): jira cloud integration does not work (#13123) 2026-03-02 22:05:29 +07:00
Mohammed Abdulai
62241e2e00 Fix: OSS suggested tasks empty state (#12563)
Co-authored-by: Mohammed Abdulai <nurud43@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-02 18:45:29 +07:00
Neha Prasad
f5197bd76a fix: prevent double scrollbar when profile avatar popover is shown (#13115)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-03-02 18:14:04 +07:00
Tim O'Farrell
e1408f7b15 Add timeout to Keycloak operations and convert OfflineTokenStore to async (#13096)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-03-02 01:48:45 -07:00
Shruti1128
d6b8d80026 Remove unused subscription-related frontend code (#12557) 2026-03-01 21:14:00 +01:00
Hiep Le
1e6a92b454 feat(backend): organizations llm settings api (org project) (#13108) 2026-03-02 00:06:37 +07:00
Hiep Le
b4a3e5db2f feat(backend): saas – organizations app settings api (#13022) 2026-03-01 23:26:39 +07:00
Chris Bagwell
f9d553d0bb Pass container port instead of host port to Docker (#12595)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2026-02-28 17:45:16 +01:00
Tim O'Farrell
f6f6c1ab25 refactor: use SQL filtering and pagination in VerifiedModelStore (#13068)
Co-authored-by: bittoby <brianwhitedev1996@gmail.com>
Co-authored-by: statxc <statxc@user.noreply.github.com>
Co-authored-by: bittoby <bittoby@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-28 07:37:11 -07:00
Hiep Le
c511a89426 feat(frontend): display Bitbucket signup disabled message on login page (#13100) 2026-02-28 19:26:16 +07:00
HeyItsChloe
1f82ff04d9 feat(frontend): SaaS NUE profile questions /Onboarding flow (#13029)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-28 13:27:22 +07:00
HeyItsChloe
eec17311c7 fix(frontend): bitbucket icon color (#13106)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-28 12:12:45 +07:00
Nelson Spence
c34fdf4b37 fix(security): extend action type coverage in security check (#12870)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2026-02-28 05:44:29 +01:00
Engel Nyst
25076ee44c chore: mark security/ and resolver/ as Legacy V0 (#13062)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-28 04:22:47 +01:00
aivong-openhands
baaec8473a Fix CVE-2024-23342: Replace python-jose with jwcrypto (#13012)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-27 22:15:23 +00:00
dependabot[bot]
402fa47422 chore(deps-dev): bump minimatch from 3.1.2 to 3.1.5 in /frontend in the security-all group across 1 directory (#13098)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 14:15:41 -06:00
dependabot[bot]
8dde385843 chore(deps): bump pypdf from 6.7.2 to 6.7.3 (#13099)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-27 13:32:02 -06:00
aivong-openhands
a905e35531 Fix CVE-2026-25990: Update pillow to 12.1.1 (#13024)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-27 12:37:25 -06:00
dependabot[bot]
1f185173b7 chore(deps): bump pypdf to 6.7.2 (#13072)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-02-27 08:35:25 -06:00
dependabot[bot]
ddc7a78723 chore(deps): bump rollup from 4.57.0 to 4.59.0 in /frontend in the security-all group across 1 directory (#13078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 08:29:19 -06:00
Dream
a29ed4d926 feat(frontend): display Agent Skills and Commands in slash menu (#12982)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-27 20:46:38 +07:00
Hiep Le
b8ab4bb44e feat(backend): allow owners to edit owners and admins to edit admins (org project) (#13095) 2026-02-27 16:01:05 +07:00
Hiep Le
ddd544f8d6 feat(backend): saas users app settings api (#13021) 2026-02-27 13:01:03 +07:00
mamoodi
3804b66e32 Remove blank issue creation (#13090) 2026-02-26 16:56:13 -05:00
John-Mason P. Shackelford
b97adf392a docs: Add plugin launch flow design document (#13084)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2026-02-26 14:03:40 -05:00
Saurya Velagapudi
dcb584913a Fix CVE-2026-26007: Update cryptography to 46.0.5 (#13059)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-26 09:45:54 -06:00
dependabot[bot]
d2fd54a083 chore(deps): bump the security-all group across 1 directory with 2 updates (#13069)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: aivong-openhands <ai.vong@openhands.dev>
2026-02-26 09:39:44 -06:00
Tim O'Farrell
112d863287 refactor: Remove unused Google Sheets integration from UserVerifier (#13076)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-26 08:07:38 -07:00
Hiep Le
c8680caec3 fix: db migration (#13074) 2026-02-26 21:20:08 +07:00
sp.wack
d4b9fb1d03 fix(backend): user email capture (#12902)
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-26 17:29:30 +04:00
Tim O'Farrell
409df1287d Fix api key access (#13064)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-26 06:22:23 -07:00
BitToby
a92bfe6cc0 feat: add database-backed verified models for dynamic model managemen… (#12833)
Co-authored-by: statxc <statxc@user.noreply.github.com>
Co-authored-by: bittoby <bittoby@users.noreply.github.com>
2026-02-26 06:17:18 -07:00
Hiep Le
f93e3254d3 refactor(frontend): remove feature flag (planning agent) (#12880) 2026-02-26 18:44:26 +07:00
Tim O'Farrell
0476d57451 fix: properly extract redirect URL from OAuth state in keycloak_offline_callback (#13063)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-26 03:23:40 -07:00
Hiep Le
a4cd21e155 refactor(frontend): hide microagent management ui (#13060) 2026-02-26 17:13:37 +07:00
aivong-openhands
7f3af371d1 protobuf pypdf uv lock updates (#13045) 2026-02-25 14:19:16 -06:00
aivong-openhands
1421794c1b Fix CVE-2026-26007: Update cryptography to 46.0.5 (#13009)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-25 14:18:16 -06:00
aivong-openhands
2fc689457c Fix CVE-2026-24486: Update python-multipart to 0.0.22 (#13015)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-25 10:15:58 -06:00
Tim O'Farrell
3161b365a8 Add sandbox_id field to conversation endpoints (#13044)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-25 14:29:42 +00:00
aivong-openhands
18ab56ef4e Fix CVE-2026-23490: Update pyasn1 to 0.6.2 (#13013)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-25 08:06:41 -06:00
Tim O'Farrell
a9c0df778c Make logs quieter. (#13042) 2026-02-25 05:11:13 -07:00
MkDev11
51b989b5f8 feat: Allow attaching/changing repository for existing conversations (#12671)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-25 18:09:12 +07:00
aivong-openhands
dc039d81d6 Fix CVE-2026-27199: Update werkzeug to 3.1.6 (#13028)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-24 18:26:28 -06:00
aivong-openhands
8e4559b14a Fix CVE-2025-61765: Update python-socketio to 5.14.0 (#13027)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-24 18:15:04 -06:00
aivong-openhands
b84f352b63 Fix CVE-2026-0994: Update protobuf to 5.29.6 (#13011)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-24 17:25:40 -06:00
aivong-openhands
a0dba6124a Fix CVE-2026-27026: Update pypdf to at least 6.7.1 (#13025)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-24 15:25:06 -06:00
aivong-openhands
951739f3eb Fix CVE-2025-53000: Update nbconvert to 7.17.0 (#13010)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
2026-02-24 14:46:31 -06:00
aivong-openhands
0f1ad46a47 Fix CVE-2025-62727: Update starlette to 0.49.1 (#13016)
Co-authored-by: OpenHands CVE Fix Bot <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2026-02-24 10:55:32 -06:00
sp.wack
5367bef43a fix: detect team/org-level budget errors in error banner (#13003) 2026-02-24 20:55:11 +04:00
Tim O'Farrell
3afeccfe7f fix: prevent token refresh deadlock with double-checked locking and timeouts (#13020)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-24 08:13:57 -07:00
Tim O'Farrell
0677c035ff Optimize get_sandbox_by_session_api_key with hash lookup (#13019)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-24 13:55:21 +00:00
Hiep Le
68165b52d9 feat(backend): add pagination and email filtering for organization members (#12999) 2026-02-24 16:02:24 +07:00
Dream
dcc8217317 feat(frontend): add mutateWithToast utility for standardized mutation toast handling (#12433)
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-24 15:06:44 +07:00
jpelletier1
d1410949ff Experiment - Add 'Add Team Members' button to Avatar menu in SaaS mode (#12647)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-23 23:06:57 +04:00
Tim O'Farrell
a6c0d80fe1 Fix: Logout on 401 error in useGitUser; downgrade provider error to warning (#12935)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-23 10:15:54 -07:00
Tim O'Farrell
0efb1db85d Bumped SDK to 1.11.5 (#13002) 2026-02-23 09:31:31 -07:00
Hiep Le
8e0f74c92c fix(backend): ensure members are removed from the corresponding litellm team when removed from an organization (#12996) 2026-02-23 18:45:31 +07:00
Hiep Le
6e1ba3d836 fix(backend): update current_org_id when removing a member from an organization (#12995) 2026-02-23 18:21:37 +07:00
Hiep Le
0ec97893d1 fix(backend): unable to delete an organization after inviting at least one member (#12993) 2026-02-23 18:21:10 +07:00
Tim O'Farrell
ddb809bc43 Add webhook endpoint authentication bypass and admin context unfiltered data access (#12956)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-23 09:28:49 +00:00
Alona
872f2b87f2 fix: add retry logic with exponential backoff to send_welcome_email (#12450)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2026-02-20 20:42:00 +00:00
Graham Neubig
ee86005a3a Align PR review workflow with software-agent-sdk (#12963)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-20 21:02:32 +01:00
Graham Neubig
d4aa30580b Migrate PR review workflow to use extensions action (#12917)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-20 18:44:02 +00:00
Tim O'Farrell
2f0e879129 Fix session_maker to accept kwargs for backward compatibility (#12960)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-20 10:56:44 -07:00
sp.wack
3bc2ef954e fix(backend): config values (#12944) 2026-02-20 17:53:35 +04:00
Ray Myers
32ab2a24c6 Remove enterprise-preview job and workflow (#12350)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-20 03:36:14 +01:00
Engel Nyst
a6e148d1e6 refactor: use consolidated pr-review action (#12801)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-20 02:08:42 +01:00
Manrique Vargas
3fc977eddd fix(mcp): skip conversation link when conversation_id is None (#12941)
Signed-off-by: machov <mv1742@nyu.edu>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2026-02-19 21:41:26 +00:00
John-Mason P. Shackelford
89a6890269 Fix URL encoding in Jira OAuth authorization URLs (#12399)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2026-02-19 21:40:29 +00:00
Hiep Le
8927ac2230 fix(backend): organization members now see correct shared credit balance (#12942) 2026-02-20 01:34:53 +07:00
Rohit Malhotra
f3429e33ca Fix Resend sync to respect deleted users (#12904)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-19 17:43:15 +00:00
Tim O'Farrell
7cd219792b Add type hints and use model objects in api_keys.py endpoints (#12939)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-19 08:40:46 -07:00
Hiep Le
2aabe2ed8c fix(backend): add organization filtering to V1 conversation queries (#12923) 2026-02-19 20:39:28 +07:00
Tim O'Farrell
731a9a813e More readable logs for local debugging (#12926) 2026-02-19 02:27:57 -07:00
Tim O'Farrell
123e556fed Added endpoint for readiness probe (#12927) 2026-02-19 02:27:35 -07:00
Chujiang
6676cae249 fix: add missing type hints and improve test logging (#12810)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 00:58:39 +01:00
Clay Arnold
fede37b496 fix: add claude-opus-4-6 to temperature/top_p guard (#12874)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:33:17 +01:00
Hiep Le
3bcd6f18df fix(backend): set user email fields from user_info during create_user (#12921) 2026-02-19 02:06:20 +07:00
Rohit Malhotra
0da18440c2 Mention free MiniMax usage and drop free credits (#12918)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-18 13:54:05 -05:00
Hiep Le
ac76e10048 refactor(backend): include current_org_id in organization list response (#12915) 2026-02-18 20:35:40 +07:00
Hiep Le
b98bae8b5f refactor(backend): rename orgmemberresponse.role_name to role (#12914) 2026-02-18 20:23:07 +07:00
Tim O'Farrell
516721d1ee fix: add default uuid4 to event_callback_result primary key (#12908)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-18 05:57:13 -07:00
Hiep Le
4d6f66ca28 feat: add user invitation logic (#12883)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-18 13:24:19 +07:00
chuckbutkus
b18568da0b Feature/permission based authorization (#12906)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-18 01:17:39 -05:00
mamoodi
83dd3c169c Release 1.4.0 (#12897) 2026-02-17 13:09:29 -05:00
Tim O'Farrell
35bddb14f1 fix: preserve import order in clean_proactive_convo_table.py (#12901)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-02-17 17:52:54 +00:00
Tim O'Farrell
e8425218e2 Remove alembic errors dumped into logs by cron jobs (#12900) 2026-02-17 17:22:54 +00:00
Rohit Malhotra
0a879fa781 Grant free credits after minimum purchase (#12899)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-17 11:00:42 -05:00
Hiep Le
41e142bbab fix(backend): system prompt override (planning agent) (#12893) 2026-02-17 16:15:26 +07:00
Engel Nyst
b06b9eedac fix: wire suggested task prompts for V1 (#12787)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-16 23:57:32 +01:00
Tim O'Farrell
a9afafa991 Default model for new users is minimax (#12889) 2026-02-16 12:24:30 -07:00
mamoodi
663ace4b39 Add saas-rel* branch pattern to ghcr-build workflow (#12888)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-16 12:27:37 -05:00
Hiep Le
2d085a6e0a fix(frontend): add auto-scroll when new messages arrive in chat (#12885) 2026-02-16 23:46:14 +07:00
Hiep Le
8b7112abe8 refactor(frontend): hide planning preview component when plan content is empty (#12879) 2026-02-16 18:35:20 +07:00
Hiep Le
34547ba947 fix(backend): enable byor key export after purchasing credits (#12862) 2026-02-16 17:02:06 +07:00
Graham Neubig
5f958ab60d fix: suppress alembic INFO logs before import to prevent Datadog misclassification (#12691)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-12 14:32:39 -05:00
Hiep Le
d7656bf1c9 refactor(backend): rename user role to member across the system (#12853) 2026-02-13 00:45:47 +07:00
Tim O'Farrell
2bc107564c Support list_files and get_trajectory for nested conversation managers (#12850)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-02-12 10:39:00 -07:00
Tim O'Farrell
85eb1e1504 Check event types before making expensive API calls in GitHub webhook handler (#12819)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-12 09:33:59 -07:00
OpenHands Bot
cd235cc8c7 Bump SDK packages to v1.11.4 (#12839)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-02-11 10:55:46 -07:00
Graham Neubig
40f52dfabc Use lowercase minimax-m2.5 for consistency (#12840)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-12 01:29:17 +08:00
Hiep Le
bab7bf85e8 fix(backend): prevent org deletion from setting current_org_id to NULL (#12817) 2026-02-12 00:15:21 +07:00
Hiep Le
c856537f65 refactor(backend): update the patch organization api to support organization name updates (#12834) 2026-02-12 00:08:43 +07:00
Graham Neubig
736f5b2255 Add MiniMax-M2.5 model support (#12835)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-11 16:57:22 +00:00
chuckbutkus
c1d9d11772 Log all exceptions in get_user() when authentication fails (#12836)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-02-11 11:49:13 -05:00
sp.wack
85244499fe fix(frontend): performance and loading state bugs (#12821) 2026-02-11 15:34:52 +00:00
Hiep Le
c55084e223 fix(backend): read RECAPTCHA_SITE_KEY from environment in V1 web client config (#12830) 2026-02-11 18:59:52 +07:00
Tim O'Farrell
e3bb75deb4 fix(enterprise): use poetry.lock for reproducible dependency builds (#12820)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: OpenHands Bot <contact@all-hands.dev>
2026-02-11 04:51:12 -07:00
Hiep Le
1948200762 chore: update sdk to the latest version (#12811) 2026-02-11 12:57:08 +07:00
Tim O'Farrell
affe0af361 Add debug logging for sandbox startup health checks (#12814)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-10 07:23:43 -07:00
Hiep Le
f20c956196 feat(backend): implement org member patch api (#12800) 2026-02-10 20:01:24 +07:00
Alexander Grattan
4a089a3a0d fix(docs): update Gray Swan API links and onboarding instructions in security README (#12809) 2026-02-10 10:14:49 +00:00
Hiep Le
aa0b2d0b74 feat(backend): add api for switching between orgs (#12799) 2026-02-10 14:22:52 +07:00
Hiep Le
bef9b80b9d fix(frontend): add missing border radius to conversation loading on first load (#12796) 2026-02-09 21:36:07 +07:00
Graham Neubig
c4a90b1f89 Fix Resend ValidationError by adding email validation (#12511)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-08 09:47:39 -05:00
sp.wack
0d13c57d9f feat(backend): org get me route (#12760)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-07 16:11:25 +07:00
Graham Neubig
b3422f1275 Add PR Review by OpenHands workflow (#12784)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 17:26:16 -05:00
Xingyao Wang
f139a9970b feat: add SANDBOX_STARTUP_GRACE_SECONDS env var for configurable startup timeout (#12741)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-07 06:12:29 +08:00
Jamie Chicago
54d156122c Add automated PR review workflow using OpenHands (#12698)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2026-02-06 19:02:55 +00:00
Tim O'Farrell
ac072bf686 feat(frontend): change alert banner from solid background to border style (#12783)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 18:05:29 +00:00
Hiep Le
a53812c029 feat(backend): develop delete /api/organizations/{orgid}/members/{userid} api (#12734) 2026-02-07 00:50:47 +07:00
Tim O'Farrell
1d1c0925b5 refactor: Move check_byor_export_enabled to OrgService and add tests (PR #12753 followup) (#12782)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 17:03:03 +00:00
Hiep Le
872f41e3c0 feat(backend): implement get /api/organizations/{orgId}/members api (#12735) 2026-02-06 23:47:30 +07:00
Tim O'Farrell
d43ff82534 feat: Add BYOR export flag to org for LLM key access control (#12753)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-06 09:30:12 -07:00
huangkevin-apr
8cd8c011b2 fix(a11y): Add aria-label to Sidebar component (#12728) 2026-02-06 22:32:52 +07:00
Tim O'Farrell
5c68b10983 (Frontend) Migrate to new /api/v1/web-client/config endpoint (#12479)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-02-06 08:31:40 -07:00
Graham Neubig
a97fad1976 fix: Add PostHog error tracking for V1 AgentErrorEvent and ConversationErrorEvent (#12543)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 09:51:01 -05:00
Graham Neubig
4c3542a91c fix: use appropriate log level for webhook installation results (#12493)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 09:01:37 -05:00
Tim O'Farrell
f460057f58 chore: add deprecation notices to all runtime directory files (#12772)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 05:15:02 -07:00
MkDev11
4fa2ad0f47 fix: add exponential backoff retry for env var export when bash session is busy (#12748)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
2026-02-06 05:07:17 -07:00
Hiep Le
dd8be12809 feat(backend): return is_personal field in OrgResponse (#12777) 2026-02-06 19:01:06 +07:00
Tim O'Farrell
89475095d9 Preload callback processor class to prevent Pydantic Deserialization Error (#12776) 2026-02-06 04:29:28 -07:00
Tim O'Farrell
05d5f8848a Fix V1 GitHub conversations failing to clone repository (#12775)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-06 03:08:55 -07:00
Hiep Le
ee2885eb0b feat: store plan.md file in appropriate configuration folders (#12713) 2026-02-06 16:09:39 +07:00
Tim O'Farrell
545257f870 Refactor: Add LLM provider utilities and improve API base URL detection (#12766)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-05 14:22:32 -07:00
Hiep Le
b23ab33a01 chore: update sdk to the latest version (#12762) 2026-02-06 00:25:11 +07:00
sp.wack
a9ede73391 fix(backend): resolve missing email and display name for user identity tracking (#12719) 2026-02-05 16:50:33 +00:00
chuckbutkus
634c2439b4 Fix key gen again (#12752)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-05 11:45:10 -05:00
Hiep Le
a1989a40b3 refactor(frontend): remove border and border radius from ConversationLoading (#12756) 2026-02-05 21:50:07 +07:00
Saurya Velagapudi
e38f1283ea feat(recaptcha): add user_id and email to assessment log (#12749)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-04 17:58:44 -08:00
Tim O'Farrell
07eb791735 Remove flaky test_bash_session.py (#12739)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-04 11:55:22 -07:00
Saurya Velagapudi
c355c4819f feat(recaptcha): add assessment name to logging and AssessmentResult (#12744)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-04 09:30:02 -08:00
sp.wack
9d8e4c44cc fix(frontend): fix cross-domain PostHog tracking param names and persist bootstrap IDs across OAuth redirects (#12729) 2026-02-04 16:15:53 +04:00
Hiep Le
25cc55e558 chore: update sdk to the latest version (#12737) 2026-02-04 01:20:13 +07:00
chuckbutkus
0e825c38d7 APP-443: Fix key generation (#12726)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
2026-02-03 17:50:40 +00:00
Graham Neubig
ce04e70b5b fix: BYOR to OpenHands provider switch auth error (#12725)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-03 09:26:56 -07:00
Tim O'Farrell
7b0589ad40 Add GitHub and Replicated app slugs to web client config (#12724) 2026-02-02 15:53:52 -07:00
mamoodi
682465a862 Release 1.3 (#12715) 2026-02-02 16:17:01 -05:00
Tim O'Farrell
1bb4c844d4 Fix runtime status error on conversation resume (#12718)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-02 21:04:45 +00:00
Neha Prasad
d6c11fe517 fix selected repo disappearing in pen repository dropdown (#12702)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-02-02 19:07:48 +00:00
Tim O'Farrell
b088d4857e Improve batch_get_app_conversations UUID handling (#12711)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-02 07:25:07 -07:00
Tim O'Farrell
0f05898d55 Deprecate V0 endpoints now handled by agent server (#12710)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-02 07:25:00 -07:00
Hiep Le
d1f0a01a57 feat(frontend): add silent WebSocket recovery for V1 conversations (#12677) 2026-02-02 19:53:33 +07:00
Hiep Le
f5a9d28999 feat(frontend): add shine text effect to Plan Preview during streaming (planning agent) (#12676) 2026-02-02 14:31:31 +07:00
Graham Neubig
afa0417608 Remove evaluation directory and benchmarking dependencies (#12666)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-02-01 11:39:29 -08:00
chuckbutkus
e688fba761 Fix check for already migrated (#12700) 2026-01-30 19:31:46 -05:00
Tim O'Farrell
d1ec5cbdf6 Fix litellm migration max_budget issue and add logging (#12697)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-30 16:35:31 -07:00
Jamie Chicago
f42625f789 Improve issue templates with best practices and clear expectations (#12632)
Co-authored-by: jamiechicago312 <jamiechicago312@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2026-01-30 21:27:44 +01:00
Graham Neubig
fe28519677 feat(frontend): add gpt-5.2 and gpt-5.2-codex models, remove gpt-5 models (#12639)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-30 12:15:47 -05:00
Tim O'Farrell
e62ceafa4a Cleaner Logs (#12579)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-30 15:25:36 +00:00
Tim O'Farrell
0b8c69fad2 Fix for issue where stats are not updated for conversations (#12688)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-30 08:12:38 -07:00
Hiep Le
37d9b672a4 fix(frontend): prevent duplicate API calls when sub-conversation becomes ready (planning agent) (#12673) 2026-01-30 15:54:51 +07:00
Hiep Le
c8b867a634 fix(frontend): persist selected agent mode across page refresh (planning agent) (#12672) 2026-01-30 15:39:36 +07:00
Hiep Le
59834beba7 fix(frontend): prevent duplicate sub-conversation creation on page refresh (planning agent) (#12645) 2026-01-30 15:25:41 +07:00
Hiep Le
d2eced9cff feat(frontend): handle the build button for the planning agent (planning agent) (#12644) 2026-01-30 15:25:16 +07:00
Hiep Le
7836136ff8 feat(frontend): disable the build button while the agent is running (planning agent) (#12643) 2026-01-30 15:13:05 +07:00
chuckbutkus
fdb04dfe5d Add GitLab provider check to schedule_gitlab_repo_sync (#12680)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-29 23:56:36 -05:00
Tim O'Farrell
3d4cb89441 fix(frontend): Support V1 conversations in MetricsModal (#12678)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-29 15:03:06 -07:00
Tim O'Farrell
9fb9efd3d2 Refactor LiteLLM key updates to use generic user key API (#12664)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-29 09:53:01 -07:00
Engel Nyst
5511c01c2e chore: clarify draft guidance in PR template (#12670)
Co-authored-by: smolpaws <engel@enyst.org>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2026-01-29 14:17:35 +00:00
Engel Nyst
02825fb5bb Cleanup CLI directory (#12669) 2026-01-29 15:14:50 +01:00
HeyItsChloe
876e773589 chore(frontend): convo tab only renders active/selected tab (#12570)
Co-authored-by: Chloe <chloe@openhands.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-29 16:47:41 +07:00
MkDev11
9e1ae86191 fix: hide Notes from task list when notes are empty (#12668)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
2026-01-29 16:13:23 +07:00
Hiep Le
df47b7b79d feat(frontend): ensure the planner tab opens when the view button is selected (#12621) 2026-01-29 13:12:09 +07:00
chuckbutkus
7d1c105b55 Move settings to dict to after openhands (#12663) 2026-01-28 23:45:01 +00:00
Tim O'Farrell
db6a9e8895 Fix broken key migration by decrypting legacy encrypted keys before LiteLLM update (#12657) 2026-01-28 15:09:50 -07:00
Hiep Le
d76ac44dc3 refactor(frontend): reduce heading text size for plan preview content (#12620) 2026-01-29 00:30:40 +07:00
MkDev11
c483c80a3c feat: add host network support for V1 DockerSandboxService (#12445)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-01-28 09:55:38 -07:00
chuckbutkus
570ab904f6 Fix UserSettings creation from Org tables (#12635)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
2026-01-28 09:35:05 -07:00
sp.wack
00a74731ae chore: refresh frontend lockfile (#12619) 2026-01-28 13:03:42 +04:00
1918 changed files with 147046 additions and 147204 deletions

View File

@@ -0,0 +1,202 @@
---
name: cross-repo-testing
description: This skill should be used when the user asks to "test a cross-repo feature", "deploy a feature branch to staging", "test SDK against OH Cloud", "e2e test a cloud workspace feature", "test provider tokens", "test secrets inheritance", or when changes span the SDK and OpenHands server repos and need end-to-end validation against a staging deployment.
triggers:
- cross-repo
- staging deployment
- feature branch deploy
- test against cloud
- e2e cloud
---
# Cross-Repo Testing: SDK ↔ OpenHands Cloud
How to end-to-end test features that span `OpenHands/software-agent-sdk` and `OpenHands/OpenHands` (the Cloud backend).
## Repository Map
| Repo | Role | What lives here |
|------|------|-----------------|
| [`software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) | Agent core | `openhands-sdk`, `openhands-workspace`, `openhands-tools` packages. `OpenHandsCloudWorkspace` lives here. |
| [`OpenHands`](https://github.com/OpenHands/OpenHands) | Cloud backend | FastAPI server (`openhands/app_server/`), sandbox management, auth, enterprise integrations. Deployed as OH Cloud. |
| [`deploy`](https://github.com/OpenHands/deploy) | Infrastructure | Helm charts + GitHub Actions that build the enterprise Docker image and deploy to staging/production. |
**Data flow:** SDK client → OH Cloud API (`/api/v1/...`) → sandbox agent-server (inside runtime container)
## When You Need This
There are **two flows** depending on which direction the dependency goes:
| Flow | When | Example |
|------|------|---------|
| **A — SDK client → new Cloud API** | The SDK calls an API that doesn't exist yet on production | `workspace.get_llm()` calling `GET /api/v1/users/me?expose_secrets=true` |
| **B — OH server → new SDK code** | The Cloud server needs unreleased SDK packages or a new agent-server image | Server consumes a new tool, agent behavior, or workspace method from the SDK |
Flow A only requires deploying the server PR. Flow B requires pinning the SDK to an unreleased commit in the server PR **and** using the SDK PR's agent-server image. Both flows may apply simultaneously.
---
## Flow A: SDK Client Tests Against New Cloud API
Use this when the SDK calls an endpoint that only exists on the server PR branch.
### A1. Write and test the server-side changes
In the `OpenHands` repo, implement the new API endpoint(s). Run unit tests:
```bash
cd OpenHands
poetry run pytest tests/unit/app_server/test_<relevant>.py -v
```
Push a PR. Wait for the **"Push Enterprise Image" (Docker) CI job** to succeed — this builds `ghcr.io/openhands/enterprise-server:sha-<COMMIT>`.
### A2. Write the SDK-side changes
In `software-agent-sdk`, implement the client code (e.g., new methods on `OpenHandsCloudWorkspace`). Run SDK unit tests:
```bash
cd software-agent-sdk
pip install -e openhands-sdk -e openhands-workspace
pytest tests/ -v
```
Push a PR. SDK CI is independent — it doesn't need the server changes to pass unit tests.
### A3. Deploy the server PR to staging
See [Deploying to a Staging Feature Environment](#deploying-to-a-staging-feature-environment) below.
### A4. Run the SDK e2e test against staging
See [Running E2E Tests Against Staging](#running-e2e-tests-against-staging) below.
---
## Flow B: OH Server Needs Unreleased SDK Code
Use this when the Cloud server depends on SDK changes that haven't been released to PyPI yet. The server's runtime containers run the `agent-server` image built from the SDK repo, so the server PR must be configured to use the SDK PR's image and packages.
### B1. Get the SDK PR merged (or identify the commit)
The SDK PR must have CI pass so its agent-server Docker image is built. The image is tagged with the **merge-commit SHA** from GitHub Actions — NOT the head-commit SHA shown in the PR.
Find the correct image tag:
- Check the SDK PR description for an `AGENT_SERVER_IMAGES` section
- Or check the "Consolidate Build Information" CI job for `"short_sha": "<tag>"`
### B2. Pin SDK packages to the commit in the OpenHands PR
In the `OpenHands` repo PR, pin all 3 SDK packages (`openhands-sdk`, `openhands-agent-server`, `openhands-tools`) to the unreleased commit and update the agent-server image tag. This involves editing 3 files and regenerating 3 lock files.
Follow the **`update-sdk` skill** → "Development: Pin SDK to an Unreleased Commit" section for the full procedure and file-by-file instructions.
### B3. Wait for the OpenHands enterprise image to build
Push the pinned changes. The OpenHands CI will build a new enterprise Docker image (`ghcr.io/openhands/enterprise-server:sha-<OH_COMMIT>`) that bundles the unreleased SDK. Wait for the "Push Enterprise Image" job to succeed.
### B4. Deploy and test
Follow [Deploying to a Staging Feature Environment](#deploying-to-a-staging-feature-environment) using the new OpenHands commit SHA.
### B5. Before merging: remove the pin
**CI guard:** `check-package-versions.yml` blocks merge to `main` if `[tool.poetry.dependencies]` contains `rev` fields. Before the OpenHands PR can merge, the SDK PR must be merged and released to PyPI, then the pin must be replaced with the released version number.
---
## Deploying to a Staging Feature Environment
The `deploy` repo creates preview environments from OpenHands PRs.
**Option A — GitHub Actions UI (preferred):**
Go to `OpenHands/deploy` → Actions → "Create OpenHands preview PR" → enter the OpenHands PR number. This creates a branch `ohpr-<PR>-<random>` and opens a deploy PR.
**Option B — Update an existing feature branch:**
```bash
cd deploy
git checkout ohpr-<PR>-<random>
# In .github/workflows/deploy.yaml, update BOTH:
# OPENHANDS_SHA: "<full-40-char-commit>"
# OPENHANDS_RUNTIME_IMAGE_TAG: "<same-commit>-nikolaik"
git commit -am "Update OPENHANDS_SHA to <commit>" && git push
```
**Before updating the SHA**, verify the enterprise Docker image exists:
```bash
gh api repos/OpenHands/OpenHands/actions/runs \
--jq '.workflow_runs[] | select(.head_sha=="<COMMIT>") | "\(.name): \(.conclusion)"' \
| grep Docker
# Must show: "Docker: success"
```
The deploy CI auto-triggers and creates the environment at:
```
https://ohpr-<PR>-<random>.staging.all-hands.dev
```
**Wait for it to be live:**
```bash
curl -s -o /dev/null -w "%{http_code}" https://ohpr-<PR>-<random>.staging.all-hands.dev/api/v1/health
# 401 = server is up (auth required). DNS may take 1-2 min on first deploy.
```
## Running E2E Tests Against Staging
**Critical: Feature deployments have their own Keycloak instance.** API keys from `app.all-hands.dev` or `$OPENHANDS_API_KEY` will NOT work. You need a test API key issued by the specific feature deployment's Keycloak.
**You (the agent) cannot obtain this key yourself** — the feature environment requires interactive browser login with credentials you do not have. You must **ask the user** to:
1. Log in to the feature deployment at `https://ohpr-<PR>-<random>.staging.all-hands.dev` in their browser
2. Generate a test API key from the UI
3. Provide the key to you so you can proceed with e2e testing
Do **not** attempt to log in via the browser or guess credentials. Wait for the user to supply the key before running any e2e tests.
```python
from openhands.workspace import OpenHandsCloudWorkspace
STAGING = "https://ohpr-<PR>-<random>.staging.all-hands.dev"
with OpenHandsCloudWorkspace(
cloud_api_url=STAGING,
cloud_api_key="<test-api-key-for-this-deployment>",
) as workspace:
# Test the new feature
llm = workspace.get_llm()
secrets = workspace.get_secrets()
print(f"LLM: {llm.model}, secrets: {list(secrets.keys())}")
```
Or run an example script:
```bash
OPENHANDS_CLOUD_API_KEY="<key>" \
OPENHANDS_CLOUD_API_URL="https://ohpr-<PR>-<random>.staging.all-hands.dev" \
python examples/02_remote_agent_server/10_cloud_workspace_saas_credentials.py
```
### Recording results
Both repos support a `.pr/` directory for temporary PR artifacts (design docs, test logs, scripts). These files are automatically removed when the PR is approved — see `.github/workflows/pr-artifacts.yml` and the "PR-Specific Artifacts" section in each repo's `AGENTS.md`.
Push test output to the `.pr/logs/` directory of whichever repo you're working in:
```bash
mkdir -p .pr/logs
python test_script.py 2>&1 | tee .pr/logs/<test_name>.log
git add -f .pr/logs/
git commit -m "docs: add e2e test results" && git push
```
Comment on **both PRs** with pass/fail summary and link to logs.
## Key Gotchas
| Gotcha | Details |
|--------|---------|
| **Feature env auth is isolated** | Each `ohpr-*` deployment has its own Keycloak. Production API keys don't work. Agents cannot log in — you must ask the user to provide a test API key from the feature deployment's UI. |
| **Two SHAs in deploy.yaml** | `OPENHANDS_SHA` and `OPENHANDS_RUNTIME_IMAGE_TAG` must both be updated. The runtime tag is `<sha>-nikolaik`. |
| **Enterprise image must exist** | The Docker CI job on the OpenHands PR must succeed before you can deploy. If it hasn't run, push an empty commit to trigger it. |
| **DNS propagation** | First deployment of a new branch takes 1-2 min for DNS. Subsequent deploys are instant. |
| **Merge-commit SHA ≠ head SHA** | SDK CI tags Docker images with GitHub Actions' merge-commit SHA, not the PR head SHA. Check the SDK PR description or CI logs for the correct tag. |
| **SDK pin blocks merge** | `check-package-versions.yml` prevents merging an OpenHands PR that has `rev` fields in `[tool.poetry.dependencies]`. The SDK must be released to PyPI first. |
| **Flow A: stock agent-server is fine** | When only the Cloud API changes, `OpenHandsCloudWorkspace` talks to the Cloud server, not the agent-server. No custom image needed. |
| **Flow B: agent-server image is required** | When the server needs new SDK code inside runtime containers, you must pin to the SDK PR's agent-server image. |

View File

@@ -0,0 +1,47 @@
---
name: custom-codereview-guide
description: Repo-specific code review guidelines for All-Hands-AI/OpenHands. Provides frontend and backend review rules in addition to the default code review skill.
triggers:
- /codereview
---
# All-Hands-AI/OpenHands Code Review Guidelines
You are an expert code reviewer for the **All-Hands-AI/OpenHands** repository. This skill provides repo-specific review guidelines.
## Frontend: i18n / Translation Key Usage
**Never dynamically construct i18n keys via string interpolation or template literals.**
All translation keys must come from the `I18nKey` enum (`frontend/src/i18n/declaration.ts`) or from canonical mapping objects like `AGENT_STATUS_MAP` (`frontend/src/utils/status.ts`). Dynamically constructed keys (e.g., `` t(`STATUS$${value.toUpperCase()}`) ``) will silently fall back to the raw key string at runtime because `i18next` returns the key itself when a translation is missing — this produces broken UI text with no build-time or test-time error.
### What to flag
- Any call to `t(...)` or `i18next.t(...)` where the key is built at runtime via template literals, string concatenation, or helper functions rather than referencing `I18nKey` or a known mapping
- Any new i18n key referenced in code that does not exist in `frontend/src/i18n/translation.json`
### Correct pattern
```ts
import { AGENT_STATUS_MAP } from "#/utils/status";
const i18nKey = AGENT_STATUS_MAP[agentState];
const message = i18nKey ? t(i18nKey) : fallback;
```
### Incorrect pattern
```ts
// BAD: constructs a key that may not exist in translation.json
const message = t(`STATUS$${agentState.toUpperCase()}`);
```
## Frontend: Data Fetching Architecture
UI components must never call API client methods (`frontend/src/api/`) directly. All data access must go through TanStack Query hooks:
```
UI components → TanStack Query hooks (frontend/src/hooks/query/ or mutation/) → API client (frontend/src/api/) → API endpoints
```
Flag any component that imports directly from `#/api/` and calls fetch/mutation functions without a TanStack Query wrapper.

View File

@@ -0,0 +1,37 @@
---
name: upcoming-release
description: This skill should be used when the user asks to "generate release notes", "list upcoming release PRs", "summarize upcoming release", "/upcoming-release", or needs to know what changes are part of an upcoming release.
---
# Upcoming Release Summary
Generate a concise summary of PRs included in the upcoming release.
## Prerequisites
Two commit SHAs are required:
- **First SHA**: The older commit (current release)
- **Second SHA**: The newer commit (what's being released)
If the user does not provide both SHAs, ask for them before proceeding.
## Workflow
1. Run the script from the repository root with the `--json` flag:
```bash
.github/scripts/find_prs_between_commits.py <older-sha> <newer-sha> --json
```
2. Filter out PRs that are:
- Chores
- Dependency updates
- Adding logs
- Refactors
3. Categorize the remaining PRs:
- **Features** - New functionality
- **Bug fixes** - Corrections to existing behavior
- **Security/CVE fixes** - Security-related changes
- **Other** - Everything else
4. Format the output with PRs listed under their category, including the PR number and a brief description.

View File

@@ -0,0 +1,123 @@
---
name: update-sdk
description: This skill should be used when the user asks to "update SDK", "bump SDK version", "pin SDK to a commit", "test unreleased SDK", "update agent-server image", "bump the version", "prepare a release", "what files change for a release", or needs to know how SDK packages are managed in the OpenHands repository. For detailed reference material, see references/docker-image-locations.md and references/sdk-pinning-examples.md in this skill directory.
---
# Update SDK
Bump SDK packages (`openhands-sdk`, `openhands-agent-server`, `openhands-tools`), pin them to unreleased commits for testing, and cut an OpenHands release.
## Quick Summary — How Many Files Change?
| Activity | Manual edits | Auto-regenerated | Total |
|----------|:------------:|:----------------:|:-----:|
| **SDK bump** (released PyPI version) | 2 | 3 | **5** |
| **SDK pin** (unreleased git commit) | 3 | 3 | **6** |
| **Release commit** (version bump) | 3 | 0 | **3** |
The 3 auto-regenerated files are always: `poetry.lock`, `uv.lock`, `enterprise/poetry.lock`.
## SDK Package Bump — 2 Files + 3 Lock Files
Land as a separate PR before the release. Examples: `929dcc3` (SDK 1.11.5), `cd235cc` (SDK 1.11.4).
| File | What to change |
|------|----------------|
| `pyproject.toml` | `openhands-sdk`, `openhands-agent-server`, `openhands-tools` in **two** sections: the `dependencies` array (PEP 508) **and** `[tool.poetry.dependencies]` |
| `openhands/app_server/sandbox/sandbox_spec_service.py` | `AGENT_SERVER_IMAGE` constant — set to `ghcr.io/openhands/agent-server:<version>-python` |
Then regenerate lock files:
```bash
poetry lock && uv lock && cd enterprise && poetry lock && cd ..
```
## Docker Image Locations — All Hardcoded References
For the complete inventory of every file containing a hardcoded Docker image tag or repository, see `references/docker-image-locations.md`. Key files that must stay in sync during an SDK bump:
| File | Image reference | Updated during SDK bump? |
|------|----------------|:------------------------:|
| `openhands/app_server/sandbox/sandbox_spec_service.py` | `AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:<tag>-python'` | ✅ Yes |
| `docker-compose.yml` | `AGENT_SERVER_IMAGE_TAG` default | ✅ Should be |
| `containers/dev/compose.yml` | `AGENT_SERVER_IMAGE_REPOSITORY` + `_TAG` defaults | ✅ Should be |
> **CI enforcement:** `.github/workflows/check-version-consistency.yml` validates version consistency and compose file image references on every PR and push to main.
### ⚠️ Docker Image Tag Gotcha (merge-commit SHA)
The SDK CI in `software-agent-sdk` repo tags Docker images with the **GitHub Actions merge-commit SHA**, NOT the PR head-commit SHA. When pinning to an SDK PR branch:
1. Check the SDK PR description for the actual image tag (look for the `AGENT_SERVER_IMAGES` section)
2. Or query the CI logs: the "Consolidate Build Information" job prints `"short_sha": "<tag>"`
3. The merge-commit SHA differs from the head SHA shown in the PR
For released SDK versions, images use a version tag (e.g., `1.12.0-python`) — no merge-commit ambiguity.
## Cutting a Release — 3 Files
A release commit updates the version string across 3 files. Gold-standard examples: 1.3.0 (`d063c8c`), 1.4.0 (`495f48b`).
| File | What to change |
|------|----------------|
| `pyproject.toml` | `version = "X.Y.Z"` under `[tool.poetry]` |
| `frontend/package.json` | `"version": "X.Y.Z"` |
| `frontend/package-lock.json` | `"version": "X.Y.Z"` in **two** places (root object and `packages[""]`) |
> **Note:** `openhands/version.py` reads the version from `pyproject.toml` at runtime — no manual edit needed there.
### Compose Files (2 files)
Both compose files should use `ghcr.io/openhands/agent-server` with the current SDK version tag.
| File | What to verify |
|------|----------------|
| `docker-compose.yml` | `AGENT_SERVER_IMAGE_REPOSITORY` defaults to agent-server, `AGENT_SERVER_IMAGE_TAG` is current |
| `containers/dev/compose.yml` | Same — must use agent-server, not runtime |
### Release Workflow
#### Step 1: Verify the SDK bump has landed
```bash
grep -n "openhands-sdk\|openhands-agent-server\|openhands-tools" pyproject.toml
grep -n "AGENT_SERVER_IMAGE" openhands/app_server/sandbox/sandbox_spec_service.py
grep "AGENT_SERVER_IMAGE_TAG" docker-compose.yml containers/dev/compose.yml
```
#### Step 2: Bump version numbers
```bash
# Edit pyproject.toml, frontend/package.json, frontend/package-lock.json
git add pyproject.toml frontend/package.json frontend/package-lock.json
git commit -m "Release X.Y.Z"
git tag X.Y.Z
```
Create a `saas-rel-X.Y.Z` branch from the tagged commit for the SaaS deployment pipeline.
#### Step 3: Images get tagged automatically
Every push to `main` / `saas-rel-*` / `oss-rel-*` builds and publishes `ghcr.io/openhands/openhands` and `ghcr.io/openhands/enterprise-server` images for that commit (tagged by SHA, short SHA, and branch name).
Pushing a git tag `X.Y.Z` then tags the images for that commit with `X.Y.Z`, `X.Y`, `X`, and `latest`. Non-semver tags just get their literal name applied.
Requires the commit to already be built. If you push the tag too early, the retag CI job fails loudly — re-run it from the Actions UI once the build completes.
## Development: Pin SDK to an Unreleased Commit
For detailed examples of all pinning formats (commit, branch, uv-only), see `references/sdk-pinning-examples.md`.
### Files to change (3 manual + 3 lock files)
| File | What to change |
|------|----------------|
| `pyproject.toml` | Pin all 3 SDK packages in **both** `dependencies` and `[tool.poetry.dependencies]` |
| `openhands/app_server/sandbox/sandbox_spec_service.py` | `AGENT_SERVER_IMAGE` — use the merge-commit SHA tag, NOT the head-commit SHA |
| `docker-compose.yml` | `AGENT_SERVER_IMAGE_TAG` default (for local development) |
| `poetry.lock` | Auto-regenerated via `poetry lock` |
| `uv.lock` | Auto-regenerated via `uv lock` |
| `enterprise/poetry.lock` | Auto-regenerated via `cd enterprise && poetry lock` |
### CI guard
The `check-package-versions.yml` workflow blocks merging to `main` if `[tool.poetry.dependencies]` contains any `rev` fields. This ensures unreleased SDK pins do not accidentally ship in a release.

View File

@@ -0,0 +1,83 @@
# Docker Image Locations — Complete Inventory
Every file in the OpenHands repository containing a hardcoded Docker image tag, repository, or version-pinned image reference. Organized by update cadence.
## Updated During SDK Bump (must change)
These files contain image tags that **must** be updated whenever the SDK version or pinned commit changes.
### `openhands/app_server/sandbox/sandbox_spec_service.py`
- **Line:** `AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:<tag>-python'`
- **Format:** `<sdk-version>-python` for releases (e.g., `1.12.0-python`), `<7-char-commit-hash>-python` for dev pins
- **Source of truth** for which agent-server image the app server pulls at runtime
- **⚠️ Gotcha:** When pinning to an SDK PR, the image tag is the **merge-commit SHA** from GitHub Actions, not the PR head-commit SHA. Check the SDK PR description or CI logs for the correct tag.
### `docker-compose.yml`
- **Lines:**
```yaml
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/agent-server}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-<tag>-python}
```
- Used by `docker compose up` for local development
### `containers/dev/compose.yml`
- **Lines:**
```yaml
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/agent-server}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-<tag>-python}
```
- Used by the dev container setup
- **Known issue:** On main as of 1.4.0, this file still points to `ghcr.io/openhands/runtime` instead of `agent-server`, and the tag is `1.2-nikolaik` (stale from the V0 era). The `check-version-consistency.yml` CI workflow catches this.
## Updated During Release Commit (version string only)
### `pyproject.toml`
- **Line:** `version = "X.Y.Z"` under `[tool.poetry]`
- The Python version is derived from this at runtime via `openhands/version.py`
### `frontend/package.json`
- **Line:** `"version": "X.Y.Z"`
### `frontend/package-lock.json`
- **Two places:** root `"version": "X.Y.Z"` and `packages[""].version`
## Dynamic References (auto-derived, no manual update)
### `openhands/version.py`
- Reads version from `pyproject.toml` at runtime → `openhands.__version__`
### `openhands/resolver/issue_resolver.py`
- Builds `ghcr.io/openhands/runtime:{openhands.__version__}-nikolaik` dynamically
### `openhands/runtime/utils/runtime_build.py`
- Base repo URL `ghcr.io/openhands/runtime` is a constant; version comes from elsewhere
### `.github/scripts/update_pr_description.sh`
- Uses `${SHORT_SHA}` variable at CI runtime, not hardcoded
### `enterprise/Dockerfile`
- `ARG BASE="ghcr.io/openhands/openhands"` — base image, version supplied at build time
## V0 Legacy Files (separate update cadence)
These reference the V0 runtime image (`ghcr.io/openhands/runtime:X.Y-nikolaik`) for local Docker/Kubernetes paths. They are **not** updated as part of a V1 release but may be updated independently.
### `Development.md`
- `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:X.Y-nikolaik`
### `openhands/runtime/impl/kubernetes/README.md`
- `runtime_container_image = "docker.openhands.dev/openhands/runtime:X.Y-nikolaik"`
### `enterprise/enterprise_local/README.md`
- Uses `ghcr.io/openhands/runtime:main-nikolaik` (points to `main`, not versioned)
### `third_party/runtime/impl/daytona/README.md`
- Uses `${OPENHANDS_VERSION}` variable, not hardcoded
## Image Registries
| Registry | Usage |
|----------|-------|
| `ghcr.io/openhands/agent-server` | V1 agent-server (sandbox) — built by SDK repo CI |
| `ghcr.io/openhands/openhands` | Main app image — built by `ghcr-build.yml` |
| `docker.openhands.dev/openhands/*` | Mirror/CDN for the above images |

View File

@@ -0,0 +1,103 @@
# SDK Pinning Examples
Examples from real commits showing how to pin SDK packages to unreleased commits, branches, or released versions.
## Pin to a Specific Commit
Example from commit `169fb76` (pinning all 3 packages to SDK commit `100e9af`):
### `dependencies` array (PEP 508 format)
```toml
"openhands-agent-server @ git+https://github.com/OpenHands/software-agent-sdk.git@100e9af#subdirectory=openhands-agent-server",
"openhands-sdk @ git+https://github.com/OpenHands/software-agent-sdk.git@100e9af#subdirectory=openhands-sdk",
"openhands-tools @ git+https://github.com/OpenHands/software-agent-sdk.git@100e9af#subdirectory=openhands-tools",
```
### `[tool.poetry.dependencies]` (Poetry format)
```toml
openhands-sdk = { git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "100e9af", subdirectory = "openhands-sdk" }
openhands-agent-server = { git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "100e9af", subdirectory = "openhands-agent-server" }
openhands-tools = { git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "100e9af", subdirectory = "openhands-tools" }
```
### `openhands/app_server/sandbox/sandbox_spec_service.py`
```python
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:<merge-commit-sha>-python'
```
**⚠️ Important:** The image tag is the **merge-commit SHA** from the SDK CI, not the commit hash used in `pyproject.toml`. Look up the correct tag from the SDK PR description or CI logs.
## Pin to a Branch
Example from commit `430ee1c` (pinning to branch `openhands/issue-2228-sdk-settings-schema`):
### `[tool.poetry.dependencies]`
```toml
openhands-sdk = { git = "https://github.com/OpenHands/software-agent-sdk.git", branch = "openhands/issue-2228-sdk-settings-schema", subdirectory = "openhands-sdk" }
openhands-agent-server = { git = "https://github.com/OpenHands/software-agent-sdk.git", branch = "openhands/issue-2228-sdk-settings-schema", subdirectory = "openhands-agent-server" }
openhands-tools = { git = "https://github.com/OpenHands/software-agent-sdk.git", branch = "openhands/issue-2228-sdk-settings-schema", subdirectory = "openhands-tools" }
```
## Using `[tool.uv.sources]` Override
When only `uv` needs the override (keep PyPI versions in the main arrays), add a `[tool.uv.sources]` section. Example from commit `1daca49`:
```toml
[tool.uv.sources]
openhands-sdk = { git = "https://github.com/OpenHands/software-agent-sdk.git", subdirectory = "openhands-sdk", rev = "4170cca" }
openhands-agent-server = { git = "https://github.com/OpenHands/software-agent-sdk.git", subdirectory = "openhands-agent-server", rev = "4170cca" }
openhands-tools = { git = "https://github.com/OpenHands/software-agent-sdk.git", subdirectory = "openhands-tools", rev = "4170cca" }
```
## Released PyPI Version (standard release)
Example from commit `929dcc3` (SDK 1.11.5):
### `dependencies` array
```toml
"openhands-agent-server==1.11.5",
"openhands-sdk==1.11.5",
"openhands-tools==1.11.5",
```
### `[tool.poetry.dependencies]`
```toml
openhands-sdk = "1.11.5"
openhands-agent-server = "1.11.5"
openhands-tools = "1.11.5"
```
### `openhands/app_server/sandbox/sandbox_spec_service.py`
For released versions, the image tag uses the version number:
```python
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:1.11.5-python'
```
However, **some releases use a commit-hash tag** even for the released version. Check which tag format exists on GHCR. Example from `929dcc3`:
```python
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:010e847-python'
```
## Regenerate Lock Files
After any change to `pyproject.toml`, always regenerate:
```bash
poetry lock
uv lock
cd enterprise && poetry lock && cd ..
```
## CI Guards
- **`check-package-versions.yml`**: Blocks merge to `main` if `[tool.poetry.dependencies]` contains `rev` fields (prevents shipping unreleased SDK pins)
- **`check-version-consistency.yml`**: Validates version strings match across `pyproject.toml`, `package.json`, `package-lock.json`, and verifies compose files use `agent-server` images

1
.gitattributes vendored
View File

@@ -4,4 +4,5 @@
* text eol=lf
# Git incorrectly thinks some media is text
*.png -text
*.gif -text
*.mp4 -text

8
.github/CODEOWNERS vendored
View File

@@ -1,8 +0,0 @@
# CODEOWNERS file for OpenHands repository
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
/frontend/ @amanape @hieptl
/openhands-ui/ @amanape @hieptl
/openhands/ @tofarr @malhotra5 @hieptl
/enterprise/ @chuckbutkus @tofarr @malhotra5
/evaluation/ @xingyaoww @neubig

View File

@@ -5,52 +5,113 @@ labels: ['bug']
body:
- type: markdown
attributes:
value: Thank you for taking the time to fill out this bug report. Please provide as much information as possible
to help us understand and address the issue effectively.
value: |
## Thank you for reporting a bug! 🐛
**Please fill out all required fields.** Issues missing critical information (version, installation method, reproduction steps, etc.) will be delayed or closed until complete details are provided.
Clear, detailed reports help us resolve issues faster.
- type: checkboxes
attributes:
label: Is there an existing issue for the same bug? (If one exists, thumbs up or comment on the issue instead).
description: Please check if an issue already exists for the bug you encountered.
label: Is there an existing issue for the same bug?
description: Please search existing issues before creating a new one. If found, react or comment to the duplicate issue instead of making a new one.
options:
- label: I have checked the existing issues.
- label: I have searched existing issues and this is not a duplicate.
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the bug and reproduction steps
description: Provide a description of the issue along with any reproduction steps.
label: Bug Description
description: Clearly describe what went wrong. Be specific and concise.
placeholder: Example - "When I run a Python task, OpenHands crashes after 30 seconds with a connection timeout error."
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: Example - "OpenHands should execute the Python script and return results."
validations:
required: false
- type: textarea
id: actual-behavior
attributes:
label: Actual Behavior
description: What actually happened?
placeholder: Example - "Connection timed out after 30 seconds, task failed with error code 500."
validations:
required: false
- type: textarea
id: reproduction-steps
attributes:
label: Steps to Reproduce
description: Provide clear, step-by-step instructions to reproduce the bug.
placeholder: |
1. Install OpenHands using Docker
2. Configure with Claude 3.5 Sonnet
3. Run command: `openhands run "write a python script"`
4. Wait 30 seconds
5. Error appears
validations:
required: false
- type: dropdown
id: installation
attributes:
label: OpenHands Installation
label: OpenHands Installation Method
description: How are you running OpenHands?
options:
- Docker command in README
- GitHub resolver
- CLI (uv tool install)
- CLI (executable binary)
- CLI (Docker)
- Local GUI (Docker web interface)
- OpenHands Cloud (app.all-hands.dev)
- SDK (Python library)
- Development workflow
- CLI
- app.all-hands.dev
- Other
default: 0
validations:
required: false
- type: input
id: installation-other
attributes:
label: If you selected "Other", please specify
description: Describe your installation method
placeholder: ex. Custom Kubernetes deployment, pip install from source, etc.
- type: input
id: openhands-version
attributes:
label: OpenHands Version
description: What version of OpenHands are you using?
placeholder: ex. 0.9.8, main, etc.
description: What version are you using? Find this in settings or by running `openhands --version`
placeholder: ex. 0.9.8, main, commit hash, etc.
validations:
required: false
- type: checkboxes
id: version-confirmation
attributes:
label: Version Confirmation
description: Bugs on older versions may already be fixed. Please upgrade before submitting.
options:
- label: "I have confirmed this bug exists on the LATEST version of OpenHands"
required: false
- type: input
id: model-name
attributes:
label: Model Name
description: What model are you using?
placeholder: ex. gpt-4o, claude-3-5-sonnet, openrouter/deepseek-r1, etc.
description: Which LLM model are you using?
placeholder: ex. gpt-4o, claude-3-5-sonnet-20241022, openrouter/deepseek-r1, etc.
validations:
required: false
- type: dropdown
id: os
@@ -60,12 +121,46 @@ body:
- MacOS
- Linux
- WSL on Windows
- Windows (Docker Desktop)
- Other
validations:
required: false
- type: input
id: browser
attributes:
label: Browser (if using web UI)
description: |
If applicable, which browser and version?
placeholder: ex. Chrome 131, Firefox 133, Safari 17.2
- type: textarea
id: logs
attributes:
label: Logs and Error Messages
description: |
**Paste relevant logs, error messages, or stack traces.** Use code blocks (```) for formatting.
LLM logs are in `logs/llm/default/`. Include timestamps if errors occurred at a specific time.
placeholder: |
```
Paste error logs here
```
- type: textarea
id: additional-context
attributes:
label: Logs, Errors, Screenshots, and Additional Context
description: Please provide any additional information you think might help. If you want to share the chat history
you can click the thumbs-down (👎) button above the input field and you will get a shareable link
(you can also click thumbs up when things are going well of course!). LLM logs will be stored in the
`logs/llm/default` folder. Please add any additional context about the problem here.
label: Screenshots and Additional Context
description: |
Add screenshots, videos, runtime environment, or other context that helps explain the issue.
💡 **Share conversation history:** In the OpenHands chat UI, click the 👎 or 👍 button (above the message input) to generate a shareable link to your conversation.
placeholder: Drag and drop screenshots here, paste links, or add additional context.
- type: markdown
attributes:
value: |
---
**Note:** Issues with incomplete information may be closed or deprioritized. Maintainers and community members have limited bandwidth and prioritize well-documented bugs that are easier to reproduce and fix. Thank you for your understanding!

2
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
# disable blank issue creation
blank_issues_enabled: false

View File

@@ -1,17 +0,0 @@
---
name: Feature Request or Enhancement
about: Suggest an idea for an OpenHands feature or enhancement
title: ''
labels: 'enhancement'
assignees: ''
---
**What problem or use case are you trying to solve?**
**Describe the UX or technical implementation you have in mind**
**Additional context**
### If you find this feature request or enhancement useful, make sure to add a 👍 to the issue

View File

@@ -0,0 +1,105 @@
name: Feature Request or Enhancement
description: Suggest a new feature or improvement for OpenHands
title: '[Feature]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
## Thank you for suggesting a feature! 💡
**Please provide detailed information.** Vague or low-effort requests may be closed. Well-documented feature requests with strong community support are more likely to be added to the roadmap.
- type: checkboxes
attributes:
label: Is there an existing feature request for this?
description: Please search existing issues and feature requests before creating a new one. If found, react or comment to the duplicate issue instead of making a new one.
options:
- label: I have searched existing issues and feature requests, and this is not a duplicate.
required: true
- type: textarea
id: problem-statement
attributes:
label: Problem or Use Case
description: What problem are you trying to solve? What use case would this feature enable?
placeholder: |
Example - "As a developer working on large codebases, I need to search across multiple files simultaneously. Currently, I have to search file-by-file which is time-consuming and inefficient."
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution
description: Describe your ideal solution. What should this feature do? How should it work?
placeholder: |
Example - "Add a global search feature that allows searching across all files in the workspace. Results should show file name, line number, and context around matches. Include regex support and filtering options."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds? What are their limitations?
placeholder: Example - "I tried using grep in the terminal, but it's not integrated with the UI and doesn't provide click-to-navigate functionality."
- type: dropdown
id: priority
attributes:
label: Priority / Severity
description: How important is this feature to your workflow?
options:
- "Critical - Blocking my work, no workaround available"
- "High - Significant impact on productivity"
- "Medium - Would improve experience"
- "Low - Nice to have"
default: 2
validations:
required: true
- type: dropdown
id: scope
attributes:
label: Estimated Scope
description: To the best of your knowledge, how complex do you think this feature would be to implement?
options:
- "Small - UI tweak, config option, or minor change"
- "Medium - New feature with moderate complexity"
- "Large - Significant feature requiring architecture changes"
- "Unknown - Not sure about the technical complexity"
default: 3
- type: dropdown
id: feature-area
attributes:
label: Feature Area
description: Which part of OpenHands does this feature relate to? If you select "Other", please specify the area in the Additional Context section below.
options:
- "Agent / AI behavior"
- "User Interface / UX"
- "CLI / Command-line interface"
- "File system / Workspace management"
- "Configuration / Settings"
- "Integrations (GitHub, GitLab, etc.)"
- "Performance / Optimization"
- "Documentation"
- "Other"
validations:
required: true
- type: textarea
id: technical-details
attributes:
label: Technical Implementation Ideas (Optional)
description: If you have technical expertise, share implementation ideas, API suggestions, or relevant technical details.
placeholder: |
Example - "Could use ripgrep library for fast search. Expose results via /api/search endpoint. Frontend can use virtualized list for rendering large result sets."
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context, screenshots, mockups, or examples that help illustrate this feature request.
placeholder: Drag and drop screenshots, mockups, or links here.

View File

@@ -0,0 +1,51 @@
name: Compute Docker image tags
description: Produce the canonical OpenHands Docker tag set (ref name, short SHA, full SHA — each in bare and `sha-` prefixed form) for a given image, with optional suffix and extra raw tags.
inputs:
image:
description: Fully qualified image name (e.g. ghcr.io/owner/openhands).
required: true
ref-name:
description: Git ref name to emit as a tag (e.g. main, pr-123, saas-rel-1.2.3).
required: true
suffix:
description: Suffix appended to every tag (e.g. -amd64, -nikolaik-arm64). Leave empty for base (multi-arch manifest) tags.
required: false
default: ""
extra-tags:
description: Additional newline-separated metadata-action tag rules (e.g. extra `type=raw,value=...` lines).
required: false
default: ""
outputs:
tags:
description: Newline-separated list of fully qualified image tags.
value: ${{ steps.meta.outputs.tags }}
labels:
description: Image labels emitted by docker/metadata-action.
value: ${{ steps.meta.outputs.labels }}
version:
description: Sanitized version string (ref-name with any suffix applied). Safe to use in docker tags.
value: ${{ steps.meta.outputs.version }}
runs:
using: composite
steps:
- name: Compute tags
id: meta
uses: docker/metadata-action@v6
env:
# Use the PR head SHA (not the merge SHA) for sha-prefixed tags.
DOCKER_METADATA_PR_HEAD_SHA: "true"
with:
images: ${{ inputs.image }}
flavor: |
latest=false
suffix=${{ inputs.suffix }}
tags: |
type=raw,value=${{ inputs.ref-name }}
type=sha,prefix=sha-
type=sha,prefix=
type=sha,format=long,prefix=sha-
type=sha,format=long,prefix=
${{ inputs.extra-tags }}

View File

@@ -0,0 +1,43 @@
name: Merge multi-arch Docker manifest
description: Build a multi-arch manifest from per-arch image tags pushed by an earlier build step.
inputs:
base-tags:
description: Newline-separated list of base tags (without architecture suffix).
required: true
archs:
description: Space-separated list of architectures (e.g. "amd64 arm64").
required: true
runs:
using: composite
steps:
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create multi-arch manifests
shell: bash
env:
BASE_TAGS: ${{ inputs.base-tags }}
ARCHS: ${{ inputs.archs }}
run: |
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
sources=""
for arch in $ARCHS; do
if ! docker buildx imagetools inspect "${tag}-${arch}" > /dev/null 2>&1; then
echo "::error::Missing image ${tag}-${arch}"
exit 1
fi
sources+=" ${tag}-${arch}"
done
echo "Creating manifest for $tag from:$sources"
docker buildx imagetools create -t "$tag" $sources
done <<< "$BASE_TAGS"

View File

@@ -4,7 +4,7 @@ updates:
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 1
open-pull-requests-limit: 5
groups:
# put packages in their own group if they have a history of breaking the build or needing to be reverted
pre-commit:
@@ -29,7 +29,7 @@ updates:
directory: "/frontend"
schedule:
interval: "daily"
open-pull-requests-limit: 1
open-pull-requests-limit: 5
groups:
docusaurus:
patterns:
@@ -51,7 +51,7 @@ updates:
schedule:
interval: "weekly"
day: "wednesday"
open-pull-requests-limit: 1
open-pull-requests-limit: 5
groups:
docusaurus:
patterns:
@@ -72,9 +72,11 @@ updates:
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "docker"
directories:
- "containers/*"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

View File

@@ -1,38 +1,46 @@
<!-- Ideally you should open a PR when it is ready for review. Draft PRs will not be reviewed -->
<!-- Keep this PR as draft until it is ready for review. -->
## Summary of PR
<!-- AI/LLM agents: be concise and specific. Do not check the box below. -->
<!-- Summarize what the PR does -->
- [ ] A human has tested these changes.
## Demo Screenshots/Videos
---
<!-- AI/LLM AGENTS: This section is intended for a human author to add screenshots or videos demonstrating the PR in action (optional). While many pull requests may be generated by AI/LLM agents, we are fine with this as long as a human author has reviewed and tested the changes to ensure accuracy and functionality. -->
## Why
## Change Type
<!-- Describe problem, motivation, etc.-->
<!-- Choose the types that apply to your PR -->
## Summary
<!-- 1-3 bullets describing what changed. -->
-
## Issue Number
<!-- Required if there is a relevant issue to this PR. -->
## How to Test
<!--
Required. Share the steps for the reviewer to be able to test your PR. e.g. You can test by running `npm install` then `npm build dev`.
If you could not test this, say why.
-->
## Video/Screenshots
<!--
Provide a video or screenshots of testing your PR. e.g. you added a new feature to the gui, show us the video of you testing it successfully.
-->
## Type
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Feature
- [ ] Refactor
- [ ] Other (dependency update, docs, typo fixes, etc.)
- [ ] Breaking change
- [ ] Docs / chore
## Checklist
<!-- AI/LLM AGENTS: This checklist is for a human author to complete. Do NOT check either of the two boxes below. Leave them unchecked until a human has personally reviewed and tested the changes. -->
## Notes
- [ ] I have read and reviewed the code and I understand what the code is doing.
- [ ] I have tested the code to the best of my ability and ensured it works as expected.
## Fixes
<!-- If this resolves an issue, link it here so it will close automatically upon merge. -->
Resolves #(issue)
## Release Notes
<!-- Check the box if this change is worth adding to the release notes. If checked, you must provide an
end-user friendly description for your change below the checkbox. -->
- [ ] Include this change in the Release Notes.
<!-- Optional: migrations, config changes, rollout concerns, follow-ups, or anything reviewers should know. -->

View File

@@ -0,0 +1,330 @@
#!/usr/bin/env python3
"""
Find all PRs that went in between two commits in the OpenHands/OpenHands repository.
Handles cherry-picks and different merge strategies.
This script is designed to run from within the OpenHands repository under .github/scripts:
.github/scripts/find_prs_between_commits.py
Usage: find_prs_between_commits <older_commit> <newer_commit> [--repo <path>]
"""
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Optional
def find_openhands_repo() -> Optional[Path]:
"""
Find the OpenHands repository.
Since this script is designed to live in .github/scripts/, it assumes
the repository root is two levels up from the script location.
Tries:
1. Repository root (../../ from script location)
2. Current directory
3. Environment variable OPENHANDS_REPO
"""
# Check repository root (assuming script is in .github/scripts/)
script_dir = Path(__file__).parent.absolute()
repo_root = (
script_dir.parent.parent
) # Go up two levels: scripts -> .github -> repo root
if (repo_root / '.git').exists():
return repo_root
# Check current directory
if (Path.cwd() / '.git').exists():
return Path.cwd()
# Check environment variable
if 'OPENHANDS_REPO' in os.environ:
repo_path = Path(os.environ['OPENHANDS_REPO'])
if (repo_path / '.git').exists():
return repo_path
return None
def run_git_command(cmd: list[str], repo_path: Path) -> str:
"""Run a git command in the repository directory and return its output."""
try:
result = subprocess.run(
cmd, capture_output=True, text=True, check=True, cwd=str(repo_path)
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f'Error running git command: {" ".join(cmd)}', file=sys.stderr)
print(f'Error: {e.stderr}', file=sys.stderr)
sys.exit(1)
def extract_pr_numbers_from_message(message: str) -> set[int]:
"""Extract PR numbers from commit message in any common format."""
# Match #12345 anywhere, including in patterns like (#12345) or "Merge pull request #12345"
matches = re.findall(r'#(\d+)', message)
return set(int(m) for m in matches)
def get_commit_info(commit_hash: str, repo_path: Path) -> tuple[str, str, str]:
"""Get commit subject, body, and author from a commit hash."""
subject = run_git_command(
['git', 'log', '-1', '--format=%s', commit_hash], repo_path
)
body = run_git_command(['git', 'log', '-1', '--format=%b', commit_hash], repo_path)
author = run_git_command(
['git', 'log', '-1', '--format=%an <%ae>', commit_hash], repo_path
)
return subject, body, author
def get_commits_between(
older_commit: str, newer_commit: str, repo_path: Path
) -> list[str]:
"""Get all commit hashes between two commits."""
commits_output = run_git_command(
['git', 'rev-list', f'{older_commit}..{newer_commit}'], repo_path
)
if not commits_output:
return []
return commits_output.split('\n')
def get_pr_info_from_github(pr_number: int, repo_path: Path) -> Optional[dict]:
"""Get PR information from GitHub API if GITHUB_TOKEN is available."""
try:
# Set up environment with GitHub token
env = os.environ.copy()
if 'GITHUB_TOKEN' in env:
env['GH_TOKEN'] = env['GITHUB_TOKEN']
result = subprocess.run(
[
'gh',
'pr',
'view',
str(pr_number),
'--json',
'number,title,author,mergedAt,baseRefName,headRefName,url',
],
capture_output=True,
text=True,
check=True,
env=env,
cwd=str(repo_path),
)
return json.loads(result.stdout)
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError):
return None
def find_prs_between_commits(
older_commit: str, newer_commit: str, repo_path: Path
) -> dict[int, dict]:
"""
Find all PRs that went in between two commits.
Returns a dictionary mapping PR numbers to their information.
"""
print(f'Repository: {repo_path}', file=sys.stderr)
print('Finding PRs between commits:', file=sys.stderr)
print(f' Older: {older_commit}', file=sys.stderr)
print(f' Newer: {newer_commit}', file=sys.stderr)
print(file=sys.stderr)
# Verify commits exist
try:
run_git_command(['git', 'rev-parse', '--verify', older_commit], repo_path)
run_git_command(['git', 'rev-parse', '--verify', newer_commit], repo_path)
except SystemExit:
print('Error: One or both commits not found in repository', file=sys.stderr)
sys.exit(1)
# Extract PRs from the older commit itself (to exclude from results)
# These PRs are already included at or before the older commit
older_subject, older_body, _ = get_commit_info(older_commit, repo_path)
older_message = f'{older_subject}\n{older_body}'
excluded_prs = extract_pr_numbers_from_message(older_message)
if excluded_prs:
print(
f'Excluding PRs already in older commit: {", ".join(f"#{pr}" for pr in sorted(excluded_prs))}',
file=sys.stderr,
)
print(file=sys.stderr)
# Get all commits between the two
commits = get_commits_between(older_commit, newer_commit, repo_path)
print(f'Found {len(commits)} commits to analyze', file=sys.stderr)
print(file=sys.stderr)
# Extract PR numbers from all commits
pr_info: dict[int, dict] = {}
commits_by_pr: dict[int, list[str]] = defaultdict(list)
for commit_hash in commits:
subject, body, author = get_commit_info(commit_hash, repo_path)
full_message = f'{subject}\n{body}'
pr_numbers = extract_pr_numbers_from_message(full_message)
for pr_num in pr_numbers:
# Skip PRs that are already in the older commit
if pr_num in excluded_prs:
continue
commits_by_pr[pr_num].append(commit_hash)
if pr_num not in pr_info:
pr_info[pr_num] = {
'number': pr_num,
'first_commit': commit_hash[:8],
'first_commit_subject': subject,
'commits': [],
'github_info': None,
}
pr_info[pr_num]['commits'].append(
{'hash': commit_hash[:8], 'subject': subject, 'author': author}
)
# Try to get additional info from GitHub API
print('Fetching additional info from GitHub API...', file=sys.stderr)
for pr_num in pr_info.keys():
github_info = get_pr_info_from_github(pr_num, repo_path)
if github_info:
pr_info[pr_num]['github_info'] = github_info
print(file=sys.stderr)
return pr_info
def print_results(pr_info: dict[int, dict]):
"""Print the results in a readable format."""
sorted_prs = sorted(pr_info.items(), key=lambda x: x[0])
print(f'{"=" * 80}')
print(f'Found {len(sorted_prs)} PRs')
print(f'{"=" * 80}')
print()
for pr_num, info in sorted_prs:
print(f'PR #{pr_num}')
if info['github_info']:
gh = info['github_info']
print(f' Title: {gh["title"]}')
print(f' Author: {gh["author"]["login"]}')
print(f' URL: {gh["url"]}')
if gh.get('mergedAt'):
print(f' Merged: {gh["mergedAt"]}')
if gh.get('baseRefName'):
print(f' Base: {gh["baseRefName"]}{gh["headRefName"]}')
else:
print(f' Subject: {info["first_commit_subject"]}')
# Show if this PR has multiple commits (cherry-picked or multiple commits)
commit_count = len(info['commits'])
if commit_count > 1:
print(
f' ⚠️ Found {commit_count} commits (possible cherry-pick or multi-commit PR):'
)
for commit in info['commits'][:3]: # Show first 3
print(f' {commit["hash"]}: {commit["subject"][:60]}')
if commit_count > 3:
print(f' ... and {commit_count - 3} more')
else:
print(f' Commit: {info["first_commit"]}')
print()
def main():
if len(sys.argv) < 3:
print('Usage: find_prs_between_commits <older_commit> <newer_commit> [options]')
print()
print('Arguments:')
print(' <older_commit> The older commit hash (or ref)')
print(' <newer_commit> The newer commit hash (or ref)')
print()
print('Options:')
print(' --json Output results in JSON format')
print(' --repo <path> Path to OpenHands repository (default: auto-detect)')
print()
print('Example:')
print(
' find_prs_between_commits c79e0cd3c7a2501a719c9296828d7a31e4030585 35bddb14f15124a3dc448a74651a6592911d99e9'
)
print()
print('Repository Detection:')
print(' The script will try to find the OpenHands repository in this order:')
print(' 1. --repo argument')
print(' 2. Repository root (../../ from script location)')
print(' 3. Current directory')
print(' 4. OPENHANDS_REPO environment variable')
print()
print('Environment variables:')
print(
' GITHUB_TOKEN Optional. If set, will fetch additional PR info from GitHub API'
)
print(' OPENHANDS_REPO Optional. Path to OpenHands repository')
sys.exit(1)
older_commit = sys.argv[1]
newer_commit = sys.argv[2]
json_output = '--json' in sys.argv
# Check for --repo argument
repo_path = None
if '--repo' in sys.argv:
repo_idx = sys.argv.index('--repo')
if repo_idx + 1 < len(sys.argv):
repo_path = Path(sys.argv[repo_idx + 1])
if not (repo_path / '.git').exists():
print(f'Error: {repo_path} is not a git repository', file=sys.stderr)
sys.exit(1)
# Auto-detect repository if not specified
if repo_path is None:
repo_path = find_openhands_repo()
if repo_path is None:
print('Error: Could not find OpenHands repository', file=sys.stderr)
print('Please either:', file=sys.stderr)
print(
' 1. Place this script in .github/scripts/ within the OpenHands repository',
file=sys.stderr,
)
print(' 2. Run from the OpenHands repository directory', file=sys.stderr)
print(
' 3. Use --repo <path> to specify the repository location',
file=sys.stderr,
)
print(' 4. Set OPENHANDS_REPO environment variable', file=sys.stderr)
sys.exit(1)
# Find PRs
pr_info = find_prs_between_commits(older_commit, newer_commit, repo_path)
if json_output:
# Output as JSON
print(json.dumps(pr_info, indent=2))
else:
# Print results in human-readable format
print_results(pr_info)
# Also print a simple list for easy copying
print(f'{"=" * 80}')
print('PR Numbers (for easy copying):')
print(f'{"=" * 80}')
sorted_pr_nums = sorted(pr_info.keys())
print(', '.join(f'#{pr}' for pr in sorted_pr_nums))
if __name__ == '__main__':
main()

View File

@@ -13,7 +13,6 @@ DOCKER_RUN_COMMAND="docker run -it --rm \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:${SHORT_SHA}-nikolaik \
--name openhands-app-${SHORT_SHA} \
docker.openhands.dev/openhands/openhands:${SHORT_SHA}"

116
.github/workflows/_build-image.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
# Reusable workflow: build a multi-arch Docker image and publish a merged manifest.
# Called per image from .github/workflows/ghcr-build.yml.
name: Build and push multi-arch image
on:
workflow_call:
inputs:
image:
description: Fully-qualified image name (e.g. "ghcr.io/all-hands-ai/openhands").
required: true
type: string
context:
description: Docker build context.
required: false
type: string
default: "."
dockerfile:
description: Path to the Dockerfile.
required: true
type: string
extra-build-args:
description: Additional build-args (newline-separated). OPENHANDS_BUILD_VERSION is added automatically.
required: false
type: string
default: ""
provenance:
description: Value passed to docker/build-push-action provenance.
required: false
type: boolean
default: false
sbom:
description: Value passed to docker/build-push-action sbom.
required: false
type: boolean
default: false
buildx-driver-opts:
description: Extra buildx driver-opts (e.g. "network=host" for enterprise).
required: false
type: string
default: ""
env:
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
RELEVANT_REF_NAME: ${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
jobs:
build:
name: Build ${{ inputs.image }} (${{ matrix.arch }})
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-22.04' }}
permissions:
contents: read
packages: write
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: ${{ inputs.buildx-driver-opts }}
- name: Compute per-arch tags
id: meta
uses: ./.github/actions/docker-image-tags
with:
image: ${{ inputs.image }}
ref-name: ${{ env.RELEVANT_REF_NAME }}
suffix: -${{ matrix.arch }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/${{ matrix.arch }}
build-args: |
OPENHANDS_BUILD_VERSION=${{ env.RELEVANT_REF_NAME }}
${{ inputs.extra-build-args }}
cache-from: |
type=registry,ref=${{ inputs.image }}:buildcache-${{ steps.meta.outputs.version }}
type=registry,ref=${{ inputs.image }}:buildcache-main-${{ matrix.arch }}
cache-to: type=registry,ref=${{ inputs.image }}:buildcache-${{ steps.meta.outputs.version }},mode=max
provenance: ${{ inputs.provenance }}
sbom: ${{ inputs.sbom }}
merge:
name: Merge ${{ inputs.image }} manifest
runs-on: ubuntu-22.04
needs: build
permissions:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Compute base tags
id: meta_base
uses: ./.github/actions/docker-image-tags
with:
image: ${{ inputs.image }}
ref-name: ${{ env.RELEVANT_REF_NAME }}
- name: Merge manifests
uses: ./.github/actions/docker-merge-manifest
with:
base-tags: ${{ steps.meta_base.outputs.tags }}
archs: "amd64 arm64"

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6

View File

@@ -0,0 +1,122 @@
name: Check Version Consistency
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
check-version-consistency:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Check version and Docker image tag consistency
run: |
python - <<'PY'
import json
import re
import sys
import tomllib
errors = []
warnings = []
# ── 1. Extract the canonical version from pyproject.toml ──────────
with open("pyproject.toml", "rb") as f:
pyproject = tomllib.load(f)
version = pyproject["tool"]["poetry"]["version"]
major_minor = ".".join(version.split(".")[:2])
print(f"📦 pyproject.toml version: {version} (major.minor: {major_minor})")
# ── 2. Check frontend/package.json ────────────────────────────────
with open("frontend/package.json") as f:
pkg = json.load(f)
if pkg["version"] != version:
errors.append(
f"frontend/package.json version is '{pkg['version']}', expected '{version}'"
)
else:
print(f" ✔ frontend/package.json: {pkg['version']}")
# ── 3. Check frontend/package-lock.json (2 places) ───────────────
with open("frontend/package-lock.json") as f:
lock = json.load(f)
for key, val in [
("root.version", lock.get("version")),
('packages[""].version', lock.get("packages", {}).get("", {}).get("version")),
]:
if val != version:
errors.append(
f"frontend/package-lock.json {key} is '{val}', expected '{version}'"
)
else:
print(f" ✔ frontend/package-lock.json {key}: {val}")
# ── 4. Check compose files use agent-server images ─────────────────
# Both compose files should use ghcr.io/.../agent-server (not runtime).
# Agent-server tags use SDK version (e.g. "1.12.0-python") or commit
# hashes (e.g. "31536c8-python") — both are acceptable.
repo_pattern = re.compile(r"AGENT_SERVER_IMAGE_REPOSITORY[^}]*:-([^}]+)")
tag_pattern = re.compile(r"AGENT_SERVER_IMAGE_TAG:-([^}]+)")
for filepath in ["docker-compose.yml", "containers/dev/compose.yml"]:
try:
with open(filepath) as f:
content = f.read()
except FileNotFoundError:
warnings.append(f"{filepath}: file not found")
continue
repos = repo_pattern.findall(content)
tags = tag_pattern.findall(content)
if not repos:
warnings.append(f"{filepath}: no AGENT_SERVER_IMAGE_REPOSITORY default found")
else:
repo = repos[0]
if "agent-server" not in repo:
errors.append(
f"{filepath}: AGENT_SERVER_IMAGE_REPOSITORY defaults to '{repo}', "
f"expected an agent-server image (not runtime)"
)
else:
print(f" ✔ {filepath} image repository: {repo}")
if not tags:
warnings.append(f"{filepath}: no AGENT_SERVER_IMAGE_TAG default found")
else:
tag = tags[0]
if not tag:
errors.append(f"{filepath}: AGENT_SERVER_IMAGE_TAG default is empty")
else:
print(f" ✔ {filepath} image tag: {tag}")
# ── 5. Report ─────────────────────────────────────────────────────
print()
if warnings:
print("⚠ Warnings:")
for w in warnings:
print(f" {w}")
print()
if errors:
print("❌ FAILED: Version inconsistencies found:\n")
for e in errors:
print(f" ✖ {e}")
print(
"\nAll version numbers and Docker image tags must be consistent."
"\nSee .agents/skills/update-sdk/SKILL.md for the full checklist."
)
sys.exit(1)
else:
print("✅ All version numbers and Docker image tags are consistent.")
PY

View File

@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install poetry via pipx
uses: abatilo/actions-poetry@v4
@@ -192,7 +192,7 @@ jobs:
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: tests/e2e/test-results/
@@ -200,7 +200,7 @@ jobs:
- name: Upload OpenHands logs
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: openhands-logs
path: |

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
@@ -34,7 +34,7 @@ jobs:
fi
- name: Find Comment
uses: peter-evans/find-comment@v3
uses: peter-evans/find-comment@v4
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -1,29 +0,0 @@
# Feature branch preview for enterprise code
name: Enterprise Preview
# Run on PRs labeled
on:
pull_request:
types: [labeled]
# Match ghcr-build.yml, but don't interrupt it.
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: false
jobs:
# This must happen for the PR Docker workflow when the label is present,
# and also if it's added after the fact. Thus, it exists in both places.
enterprise-preview:
name: Enterprise preview
if: github.event.label.name == 'deploy'
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
# This should match the version in ghcr-build.yml
- name: Trigger remote job
run: |
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer ${{ secrets.ALLHANDS_BOT_GITHUB_PAT }}" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"main\", \"inputs\": {\"openhandsPrNumber\": \"${{ github.event.pull_request.number }}\", \"deployEnvironment\": \"feature\", \"enterpriseImageTag\": \"pr-${{ github.event.pull_request.number }}\" }}" \
https://api.github.com/repos/OpenHands/deploy/actions/workflows/deploy.yaml/dispatches

View File

@@ -17,18 +17,20 @@ concurrency:
jobs:
fe-e2e-test:
name: FE E2E Tests
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
strategy:
matrix:
node-version: [22]
fail-fast: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Node.js
uses: useblacksmith/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: ./frontend
run: npm ci
@@ -39,7 +41,7 @@ jobs:
working-directory: ./frontend
run: npx playwright test --project=chromium
- name: Upload Playwright report
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: always()
with:
name: playwright-report

View File

@@ -21,18 +21,20 @@ jobs:
# Run frontend unit tests
fe-test:
name: FE Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
strategy:
matrix:
node-version: [22]
fail-fast: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Node.js
uses: useblacksmith/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: ./frontend
run: npm ci

View File

@@ -1,16 +1,13 @@
# Workflow that builds, tests and then pushes the OpenHands and runtime docker images to the ghcr.io repository
# Workflow that builds and pushes the OpenHands app and enterprise Docker images to ghcr.io.
# Per-image build logic lives in .github/workflows/_build-image.yml.
name: Docker
# Always run on "main"
# Always run on tags
# Always run on PRs
# Can also be triggered manually
on:
push:
branches:
- main
tags:
- "*"
- "saas-rel-*"
- "oss-rel-*"
pull_request:
workflow_dispatch:
inputs:
@@ -19,262 +16,45 @@ on:
required: true
default: ""
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
# PR events share a group so pushes supersede each other; each commit on a release branch gets its own group.
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
env:
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
jobs:
define-matrix:
runs-on: blacksmith
outputs:
base_image: ${{ steps.define-base-images.outputs.base_image }}
steps:
- name: Define base images
shell: bash
id: define-base-images
run: |
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
]')
else
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
]')
fi
echo "base_image=$json" >> "$GITHUB_OUTPUT"
# Builds the OpenHands Docker images
ghcr_build_app:
name: Build App Image
runs-on: blacksmith-4vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.7.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Build and push app image
if: "!github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --push
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Runtime Image
runs-on: blacksmith-8vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
contents: read
packages: write
needs: define-matrix
strategy:
matrix:
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.7.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: "3.12"
cache: poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Create source distribution and Dockerfile
run: poetry run python3 -m openhands.runtime.utils.runtime_build --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Short SHA
run: |
echo SHORT_SHA=$(git rev-parse --short "$RELEVANT_SHA") >> $GITHUB_ENV
- name: Determine docker build params
if: github.event.pull_request.head.repo.fork != true
shell: bash
run: |
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} -t ${{ matrix.base_image.tag }} --dry
DOCKER_BUILD_JSON=$(jq -c . < docker-build-dry.json)
echo "DOCKER_TAGS=$(echo "$DOCKER_BUILD_JSON" | jq -r '.tags | join(",")')" >> $GITHUB_ENV
echo "DOCKER_PLATFORM=$(echo "$DOCKER_BUILD_JSON" | jq -r '.platform')" >> $GITHUB_ENV
echo "DOCKER_BUILD_ARGS=$(echo "$DOCKER_BUILD_JSON" | jq -r '.build_args | join(",")')" >> $GITHUB_ENV
- name: Build and push runtime image ${{ matrix.base_image.image }}
if: github.event.pull_request.head.repo.fork != true
uses: useblacksmith/build-push-action@v1
with:
push: true
tags: ${{ env.DOCKER_TAGS }}
platforms: ${{ env.DOCKER_PLATFORM }}
# Caching directives to boost performance
cache-from: type=registry,ref=ghcr.io/${{ env.REPO_OWNER }}/runtime:buildcache-${{ matrix.base_image.tag }}
cache-to: type=registry,ref=ghcr.io/${{ env.REPO_OWNER }}/runtime:buildcache-${{ matrix.base_image.tag }},mode=max
build-args: ${{ env.DOCKER_BUILD_ARGS }}
context: containers/runtime
provenance: false
# Forked repos can't push to GHCR, so we just build in order to populate the cache for rebuilding
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Upload runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@v6
with:
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
ghcr_build_enterprise:
name: Push Enterprise Image
runs-on: blacksmith-8vcpu-ubuntu-2204
permissions:
contents: read
packages: write
needs: [define-matrix, ghcr_build_app]
# Do not build enterprise in forks
build_app:
name: App
if: github.event.pull_request.head.repo.fork != true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
uses: ./.github/workflows/_build-image.yml
with:
image: ghcr.io/openhands/openhands
dockerfile: containers/app/Dockerfile
# Set up Docker Buildx for better performance
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/openhands/enterprise-server
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=sha,format=long
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
flavor: |
latest=auto
prefix=
suffix=
env:
DOCKER_METADATA_PR_HEAD_SHA: true
- name: Determine app image tag
shell: bash
run: |
# Duplicated with build.sh
sanitized_ref_name=$(echo "$GITHUB_REF_NAME" | sed 's/[^a-zA-Z0-9.-]\+/-/g')
OPENHANDS_BUILD_VERSION=$sanitized_ref_name
sanitized_ref_name=$(echo "$sanitized_ref_name" | tr '[:upper:]' '[:lower:]') # lower case is required in tagging
echo "OPENHANDS_DOCKER_TAG=${sanitized_ref_name}" >> $GITHUB_ENV
- name: Build and push Docker image
uses: useblacksmith/build-push-action@v1
with:
context: .
file: enterprise/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
OPENHANDS_VERSION=${{ env.OPENHANDS_DOCKER_TAG }}
platforms: linux/amd64
# Add build provenance
provenance: true
# Add build attestations for better security
sbom: true
enterprise-preview:
name: Enterprise preview
if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy')
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [ghcr_build_enterprise]
steps:
# This should match the version in enterprise-preview.yml
- name: Trigger remote job
run: |
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer ${{ secrets.ALLHANDS_BOT_GITHUB_PAT }}" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"main\", \"inputs\": {\"openhandsPrNumber\": \"${{ github.event.pull_request.number }}\", \"deployEnvironment\": \"feature\", \"enterpriseImageTag\": \"pr-${{ github.event.pull_request.number }}\" }}" \
https://api.github.com/repos/OpenHands/deploy/actions/workflows/deploy.yaml/dispatches
# "All Runtime Tests Passed" is a required job for PRs to merge
# We can remove this once the config changes
runtime_tests_check_success:
name: All Runtime Tests Passed
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: All tests passed
run: echo "All runtime tests have passed successfully!"
build_enterprise:
name: Enterprise
if: github.event.pull_request.head.repo.fork != true
needs: build_app
uses: ./.github/workflows/_build-image.yml
with:
image: ghcr.io/openhands/enterprise-server
dockerfile: enterprise/Dockerfile
extra-build-args: OPENHANDS_VERSION=sha-${{ github.event.pull_request.head.sha || github.sha }}
provenance: true
sbom: true
buildx-driver-opts: network=host
update_pr_description:
name: Update PR Description
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
needs: [ghcr_build_runtime]
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: build_app
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get short SHA
id: short_sha
run: echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
run: echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT"
- name: Update PR Description
env:
@@ -285,4 +65,4 @@ jobs:
shell: bash
run: |
echo "Updating PR description with Docker and uvx commands"
bash ${GITHUB_WORKSPACE}/.github/scripts/update_pr_description.sh
bash "${GITHUB_WORKSPACE}/.github/scripts/update_pr_description.sh"

View File

@@ -9,12 +9,12 @@ jobs:
lint-fix-frontend:
if: github.event.label.name == 'lint-fix'
name: Fix frontend linting issues
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -22,13 +22,14 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: |
cd frontend
npm install --frozen-lockfile
working-directory: ./frontend
run: npm ci
- name: Generate i18n and route types
run: |
cd frontend
@@ -58,12 +59,12 @@ jobs:
lint-fix-python:
if: github.event.label.name == 'lint-fix'
name: Fix Python linting issues
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -71,7 +72,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up python
uses: useblacksmith/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: "pip"

View File

@@ -19,34 +19,35 @@ jobs:
# Run lint on the frontend code
lint-frontend:
name: Lint frontend
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: |
cd frontend
npm install --frozen-lockfile
working-directory: ./frontend
run: npm ci
- name: Lint, TypeScript compilation, and translation checks
run: |
cd frontend
npm run lint
npm run make-i18n && tsc
npm run make-i18n && npx tsc
npm run check-translation-completeness
# Run lint on the python code
lint-python:
name: Lint python
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up python
uses: useblacksmith/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: "pip"
@@ -57,13 +58,13 @@ jobs:
lint-enterprise-python:
name: Lint enterprise python
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up python
uses: useblacksmith/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: "pip"

View File

@@ -18,7 +18,7 @@ concurrency:
jobs:
check-version:
name: Check if version has changed
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
defaults:
run:
shell: bash
@@ -27,7 +27,7 @@ jobs:
current-version: ${{ steps.version-check.outputs.current-version }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 2 # Need previous commit to compare
@@ -55,7 +55,7 @@ jobs:
publish:
name: Publish to npm
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
needs: check-version
if: needs.check-version.outputs.should-publish == 'true'
defaults:
@@ -63,7 +63,7 @@ jobs:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2

View File

@@ -86,7 +86,7 @@ jobs:
runs-on: "${{ inputs.runner || 'ubuntu-latest' }}"
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
@@ -131,7 +131,7 @@ jobs:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
run: |
@@ -185,16 +185,16 @@ jobs:
fi
echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.PAT_TOKEN || github.token }}" >> $GITHUB_ENV
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}" >> $GITHUB_ENV
echo "SANDBOX_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV
# Set branch variables
echo "TARGET_BRANCH=${{ inputs.target_branch || 'main' }}" >> $GITHUB_ENV
- name: Comment on issue with start message
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}
script: |
const issueType = process.env.ISSUE_TYPE;
github.rest.issues.createComment({
@@ -206,7 +206,7 @@ jobs:
- name: Install OpenHands
id: install_openhands
uses: actions/github-script@v7
uses: actions/github-script@v9
env:
COMMENT_BODY: ${{ github.event.comment.body || '' }}
REVIEW_BODY: ${{ github.event.review.body || '' }}
@@ -242,7 +242,7 @@ jobs:
- name: Attempt to resolve issue
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }}
GITHUB_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
@@ -269,7 +269,7 @@ jobs:
fi
- name: Upload output.jsonl as artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: always() # Upload even if the previous steps fail
with:
name: resolver-output
@@ -279,7 +279,7 @@ jobs:
- name: Create draft PR or push branch
if: always() # Create PR or branch even if the previous steps fail
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }}
GITHUB_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
@@ -305,13 +305,13 @@ jobs:
# Step leaves comment for when agent is invoked on PR
- name: Analyze Push Logs (Updated PR or No Changes) # Skip comment if PR update was successful OR leave comment if the agent made no code changes
uses: actions/github-script@v7
uses: actions/github-script@v9
if: always()
env:
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}
script: |
const fs = require('fs');
const issueNumber = process.env.ISSUE_NUMBER;
@@ -341,14 +341,14 @@ jobs:
# Step leaves comment for when agent is invoked on issue
- name: Comment on issue # Comment link to either PR or branch created by agent
uses: actions/github-script@v7
uses: actions/github-script@v9
if: always() # Comment on issue even if the previous steps fail
env:
AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
RESOLUTION_SUCCESS: ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}
script: |
const fs = require('fs');
const path = require('path');
@@ -416,12 +416,12 @@ jobs:
# Leave error comment when both PR/Issue comment handling fail
- name: Fallback Error Comment
uses: actions/github-script@v7
uses: actions/github-script@v9
if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps
env:
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
with:
github-token: ${{ secrets.PAT_TOKEN || github.token }}
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || github.token }}
script: |
const issueNumber = process.env.ISSUE_NUMBER;

136
.github/workflows/pr-artifacts.yml vendored Normal file
View File

@@ -0,0 +1,136 @@
---
name: PR Artifacts
on:
workflow_dispatch: # Manual trigger for testing
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
pull_request_review:
types: [submitted]
jobs:
# Auto-remove .pr/ directory when a reviewer approves
cleanup-on-approval:
concurrency:
group: cleanup-pr-artifacts-${{ github.event.pull_request.number }}
cancel-in-progress: false
if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Check if fork PR
id: check-fork
run: |
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.event.pull_request.base.repo.full_name }}" ]; then
echo "is_fork=true" >> $GITHUB_OUTPUT
echo "::notice::Fork PR detected - skipping auto-cleanup (manual removal required)"
else
echo "is_fork=false" >> $GITHUB_OUTPUT
fi
- uses: actions/checkout@v6
if: steps.check-fork.outputs.is_fork == 'false'
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
- name: Remove .pr/ directory
id: remove
if: steps.check-fork.outputs.is_fork == 'false'
run: |
if [ -d ".pr" ]; then
git config user.name "allhands-bot"
git config user.email "allhands-bot@users.noreply.github.com"
git rm -rf .pr/
git commit -m "chore: Remove PR-only artifacts [automated]"
git push || {
echo "::error::Failed to push cleanup commit. Check branch protection rules."
exit 1
}
echo "removed=true" >> $GITHUB_OUTPUT
echo "::notice::Removed .pr/ directory"
else
echo "removed=false" >> $GITHUB_OUTPUT
echo "::notice::No .pr/ directory to remove"
fi
- name: Update PR comment after cleanup
if: steps.check-fork.outputs.is_fork == 'false' && steps.remove.outputs.removed == 'true'
uses: actions/github-script@v9
with:
script: |
const marker = '<!-- pr-artifacts-notice -->';
const body = `${marker}
✅ **PR Artifacts Cleaned Up**
The \`.pr/\` directory has been automatically removed.
`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body,
});
}
# Warn if .pr/ directory exists (will be auto-removed on approval)
check-pr-artifacts:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Check for .pr/ directory
id: check
run: |
if [ -d ".pr" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "::warning::.pr/ directory exists and will be automatically removed when the PR is approved. For fork PRs, manual removal is required before merging."
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Post or update PR comment
if: steps.check.outputs.exists == 'true'
uses: actions/github-script@v9
with:
script: |
const marker = '<!-- pr-artifacts-notice -->';
const body = `${marker}
📁 **PR Artifacts Notice**
This PR contains a \`.pr/\` directory with PR-specific documents. This directory will be **automatically removed** when the PR is approved.
> For fork PRs: Manual removal is required before merging.
`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (!existing) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body,
});
}

View File

@@ -0,0 +1,48 @@
---
name: PR Review by OpenHands
on:
# TEMPORARY MITIGATION (Clinejection hardening)
#
# We temporarily avoid `pull_request_target` here. We'll restore it after the PR review
# workflow is fully hardened for untrusted execution.
pull_request:
types: [opened, ready_for_review, labeled, review_requested]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
pr-review:
# Note: fork PRs will not have access to repository secrets under `pull_request`.
# Skip forks to avoid noisy failures until we restore a hardened `pull_request_target` flow.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
(
(github.event.action == 'opened' && github.event.pull_request.draft == false) ||
github.event.action == 'ready_for_review' ||
(github.event.action == 'labeled' && github.event.label.name == 'review-this') ||
(
github.event.action == 'review_requested' &&
(
github.event.requested_reviewer.login == 'openhands-agent' ||
github.event.requested_reviewer.login == 'all-hands-bot'
)
)
)
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-24.04
steps:
- name: Run PR Review
uses: OpenHands/extensions/plugins/pr-review@main
with:
llm-model: litellm_proxy/claude-sonnet-4-5-20250929
llm-base-url: https://llm-proxy.app.all-hands.dev
review-style: roasted
llm-api-key: ${{ secrets.LLM_API_KEY }}
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
lmnr-api-key: ${{ secrets.LMNR_SKILLS_API_KEY }}

View File

@@ -0,0 +1,85 @@
---
name: PR Review Evaluation
# This workflow evaluates how well PR review comments were addressed.
# It runs when a PR is closed to assess review effectiveness.
#
# Security note: pull_request_target is safe here because:
# 1. Only triggers on PR close (not on code changes)
# 2. Does not checkout PR code - only downloads artifacts from trusted workflow runs
# 3. Runs evaluation scripts from the extensions repo, not from the PR
on:
pull_request_target:
types: [closed]
permissions:
contents: read
pull-requests: read
jobs:
evaluate:
runs-on: ubuntu-24.04
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_NAME: ${{ github.repository }}
PR_MERGED: ${{ github.event.pull_request.merged }}
steps:
- name: Download review trace artifact
id: download-trace
uses: dawidd6/action-download-artifact@v15
continue-on-error: true
with:
workflow: pr-review-by-openhands.yml
name: pr-review-trace-${{ github.event.pull_request.number }}
path: trace-info
search_artifacts: true
if_no_artifact_found: warn
- name: Check if trace file exists
id: check-trace
run: |
if [ -f "trace-info/laminar_trace_info.json" ]; then
echo "trace_exists=true" >> $GITHUB_OUTPUT
echo "Found trace file for PR #$PR_NUMBER"
else
echo "trace_exists=false" >> $GITHUB_OUTPUT
echo "No trace file found for PR #$PR_NUMBER - skipping evaluation"
fi
# Always checkout main branch for security - cannot test script changes in PRs
- name: Checkout extensions repository
if: steps.check-trace.outputs.trace_exists == 'true'
uses: actions/checkout@v6
with:
repository: OpenHands/extensions
path: extensions
- name: Set up Python
if: steps.check-trace.outputs.trace_exists == 'true'
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install dependencies
if: steps.check-trace.outputs.trace_exists == 'true'
run: pip install lmnr
- name: Run evaluation
if: steps.check-trace.outputs.trace_exists == 'true'
env:
# Script expects LMNR_PROJECT_API_KEY; org secret is named LMNR_SKILLS_API_KEY
LMNR_PROJECT_API_KEY: ${{ secrets.LMNR_SKILLS_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python extensions/plugins/pr-review/scripts/evaluate_review.py \
--trace-file trace-info/laminar_trace_info.json
- name: Upload evaluation logs
uses: actions/upload-artifact@v7
if: always() && steps.check-trace.outputs.trace_exists == 'true'
with:
name: pr-review-evaluation-${{ github.event.pull_request.number }}
path: '*.log'
retention-days: 30

View File

@@ -19,7 +19,7 @@ jobs:
# Run python tests on Linux
test-on-linux:
name: Python Tests on Linux
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04
env:
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
strategy:
@@ -30,20 +30,22 @@ jobs:
pull-requests: write
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux
- name: Setup Node.js
uses: useblacksmith/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
@@ -63,7 +65,7 @@ jobs:
env:
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: coverage-openhands
path: |
@@ -73,16 +75,16 @@ jobs:
test-enterprise:
name: Enterprise Python Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
@@ -95,7 +97,7 @@ jobs:
env:
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: coverage-enterprise
path: ".coverage.enterprise.${{ matrix.python_version }}"
@@ -111,9 +113,9 @@ jobs:
pull-requests: write
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v8
id: download
with:
pattern: coverage-*

View File

@@ -17,14 +17,14 @@ on:
jobs:
release:
runs-on: blacksmith-4vcpu-ubuntu-2204
# Run when manually dispatched for "app server" OR for tag pushes that don't contain '-cli'
runs-on: ubuntu-22.04
# Run when manually dispatched for "app server" OR for tag pushes that don't contain '-cli' and don't start with 'cloud-'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'app server')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli'))
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli') && !startsWith(github.ref, 'refs/tags/cloud-'))
steps:
- uses: actions/checkout@v4
- uses: useblacksmith/setup-python@v6
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Poetry

View File

@@ -8,10 +8,10 @@ on:
jobs:
stale:
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
if: github.repository == 'OpenHands/OpenHands'
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
stale-issue-message: 'This issue is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
stale-pr-message: 'This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'

59
.github/workflows/tag-image.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
# Adds a git-tag name to existing Docker images.
# Triggered when a tag is pushed: finds the images built at the tag's commit
# (tagged `sha-<full>`) and adds the tag name as an alias for the same manifest.
# Semver tags (X.Y.Z) also get X.Y, X, and latest aliases.
# No rebuild — pure registry-side retag via `docker buildx imagetools create`.
name: Tag Docker images
on:
push:
tags:
- "*"
jobs:
retag:
runs-on: ubuntu-22.04
permissions:
packages: write
strategy:
matrix:
image:
- ghcr.io/openhands/openhands
- ghcr.io/openhands/enterprise-server
steps:
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Compute tags
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ matrix.image }}
flavor: latest=auto
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Add tags to existing image
env:
SRC: ${{ matrix.image }}:sha-${{ github.sha }}
TAGS: ${{ steps.meta.outputs.tags }}
shell: bash
run: |
set -euo pipefail
if ! docker buildx imagetools inspect "$SRC" > /dev/null 2>&1; then
echo "::error::Source image $SRC does not exist. The Docker workflow for commit ${{ github.sha }} may not have completed successfully. Re-run this workflow once the build finishes."
exit 1
fi
args=()
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
args+=(-t "$tag")
done <<< "$TAGS"
docker buildx imagetools create "${args[@]}" "$SRC"

View File

@@ -19,10 +19,10 @@ concurrency:
jobs:
ui-build:
name: Build openhands-ui
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: "openhands-ui/.bun-version"

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Check if welcome comment already exists
id: check_comment
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
result-encoding: string
script: |
@@ -33,7 +33,7 @@ jobs:
- name: Leave welcome comment
if: steps.check_comment.outputs.result == 'false'
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
script: |
const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`;

6
.gitignore vendored
View File

@@ -234,6 +234,8 @@ yarn-error.log*
logs
ralph/
# agent
.envrc
/workspace
@@ -252,10 +254,6 @@ run_instance_logs
runtime_*.tar
# docker build
containers/runtime/Dockerfile
containers/runtime/project.tar.gz
containers/runtime/code
**/node_modules/
# test results

112
AGENTS.md
View File

@@ -13,6 +13,14 @@ export RUNTIME=local
make build && make run FRONTEND_PORT=12000 FRONTEND_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0 &> /tmp/openhands-log.txt &
```
Local run troubleshooting notes:
- If the backend fails with `nc: command not found`, install `netcat-openbsd`.
- If local runtime startup fails with `duplicate session: test-session`, clear the stale tmux session on the default socket: `tmux -S /tmp/tmux-$(id -u)/default kill-session -t test-session`.
- Local runtime browser startup expects Playwright browsers under `~/.cache/playwright`; if needed run `PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright poetry run playwright install chromium`.
- In this sandbox environment, an inherited `SESSION_API_KEY` can make `/api/v1/settings` return 401 in the browser. Unset it before `make run` when you want to use the local web UI directly.
- In this sandbox, `frontend`'s `npm run dev:mock` / `dev:mock:saas` can start but still be awkward to browse through the work-host proxy. For PR QA screenshots, a reliable fallback is to `npm run build` with the desired `VITE_MOCK_*` env, then serve `build/` with a tiny custom HTTP server that returns the minimal mock JSON endpoints needed by the settings page.
IMPORTANT: Before making any changes to the codebase, ALWAYS run `make install-pre-commit-hooks` to ensure pre-commit hooks are properly installed.
Before pushing any changes, you MUST ensure that any lint errors or simple test errors have been fixed.
@@ -36,9 +44,81 @@ then re-run the command to ensure it passes. Common issues include:
- Be especially careful with `git reset --hard` after staging files, as it will remove accidentally staged files
- When remote has new changes, use `git fetch upstream && git rebase upstream/<branch>` on the same branch
## Lockfile Regeneration (Preserve Original Tool Versions)
When regenerating lockfiles (poetry.lock, uv.lock, etc.), you MUST use the same tool version that originally generated the lockfile to avoid unnecessary diff noise. Each lockfile contains a version header indicating which tool version was used.
### Poetry (poetry.lock)
1. Extract the version from the lockfile header:
```bash
POETRY_VERSION=$(grep -m1 "^# This file is automatically @generated by Poetry" poetry.lock | sed 's/.*Poetry \([0-9.]*\).*/\1/')
```
2. If a version is found, install that specific version:
```bash
pipx install poetry==$POETRY_VERSION --force
```
3. Then regenerate the lockfile:
```bash
poetry lock --no-update
```
### uv (uv.lock)
1. Extract the version from the lockfile header:
```bash
UV_VERSION=$(grep -m1 "^# This file was autogenerated by uv" uv.lock | sed 's/.*uv version \([0-9.]*\).*/\1/')
```
2. If a version is found, install that specific version:
```bash
pipx install uv==$UV_VERSION --force
```
3. Then regenerate the lockfile:
```bash
uv lock
```
This ensures that lockfile updates only contain actual dependency changes, not tool version migration artifacts.
## PR-Specific Artifacts (`.pr/` directory)
When working on a PR that requires design documents, scripts meant for development-only, or other temporary artifacts that should NOT be merged to main, store them in a `.pr/` directory at the repository root.
### Usage
```
.pr/
├── design.md # Design decisions and architecture notes
├── analysis.md # Investigation or debugging notes
├── logs/ # Test output or CI logs for reviewer reference
└── notes.md # Any other PR-specific content
```
### How It Works
1. **Notification**: When `.pr/` exists, a comment is posted to the PR conversation alerting reviewers
2. **Auto-cleanup**: When the PR is approved, the `.pr/` directory is automatically removed via `.github/workflows/pr-artifacts.yml`
3. **Fork PRs**: Auto-cleanup cannot push to forks, so manual removal is required before merging
### Important Notes
- Do NOT put anything in `.pr/` that needs to be preserved after merge
- The `.pr/` check passes (green ✅) during development — it only posts a notification, not a blocking error
- For fork PRs: You must manually remove `.pr/` before the PR can be merged
### When to Use
- Complex refactoring that benefits from written design rationale
- Debugging sessions where you want to document your investigation
- E2E test results or logs that demonstrate a cross-repo feature works
- Feature implementations that need temporary planning docs
- Any analysis that helps reviewers understand the PR but isn't needed long-term
## Repository Structure
Backend:
- Located in the `openhands` directory
- The current V1 application server lives in `openhands/app_server/`. `make start-backend` still launches `openhands.server.listen:app`, which includes the V1 routes by default unless `ENABLE_V1=0`.
- For V1 web-app docs, LLM setup should point users to the Settings UI.
- Testing:
- All tests are in `tests/unit/test_*.py`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
@@ -66,6 +146,8 @@ Frontend:
- Query hooks should follow the pattern use[Resource] (e.g., `useConversationSkills`)
- Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`)
- Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints
- For SaaS organization management screens, prefer deriving the selected organization from `useOrganizations()` plus the selected org ID store instead of adding a dedicated single-org fetch when only list-level fields (for example `name`) are needed.
VSCode Extension:
- Located in the `openhands/integrations/vscode` directory
@@ -154,6 +236,7 @@ Each integration follows a consistent pattern with service classes, storage mode
- Database changes require careful migration planning in `enterprise/migrations/`
- Always test changes in both OpenHands and enterprise contexts
- Use the enterprise-specific Makefile commands for development
- When the `openhands-ai` package (root project) version has been updated, run `poetry lock` in the `enterprise/` folder to update the version in the enterprise poetry lockfile.
**Enterprise Testing Best Practices:**
@@ -165,7 +248,7 @@ Each integration follows a consistent pattern with service classes, storage mode
**Import Patterns:**
- Use relative imports without `enterprise.` prefix in enterprise code
- Example: `from storage.database import session_maker` not `from enterprise.storage.database import session_maker`
- Example: `from storage.database import a_session_maker` not `from enterprise.storage.database import a_session_maker`
- This ensures code works in both OpenHands and enterprise contexts
**Test Structure:**
@@ -342,3 +425,30 @@ To add a new LLM model to OpenHands, you need to update multiple files across bo
- Models appear in CLI provider selection based on the verified arrays
- The `organize_models_and_providers` function groups models by provider
- Default model selection prioritizes verified models for each provider
### Sandbox Settings API (SDK Credential Inheritance)
The sandbox settings API allows SDK-created conversations to inherit the user's SaaS credentials
(LLM config, secrets) securely via `LookupSecret`. Raw secret values only flow SaaS→sandbox,
never through the SDK client.
#### User Credentials with Exposed Secrets (in `openhands/app_server/user/user_router.py`):
- `GET /api/v1/users/me?expose_secrets=true` → Full user settings with unmasked secrets (e.g., `llm_api_key`)
- `GET /api/v1/users/me` → Full user settings (secrets masked, Bearer only)
Auth requirements for `expose_secrets=true`:
- Bearer token (proves user identity via `OPENHANDS_API_KEY`)
- `X-Session-API-Key` header (proves caller has an active sandbox owned by the authenticated user)
Called by `workspace.get_llm()` in the SDK to retrieve LLM config with the API key.
#### Sandbox-Scoped Secrets Endpoints (in `openhands/app_server/sandbox/sandbox_router.py`):
- `GET /sandboxes/{id}/settings/secrets` → list secret names (no values)
- `GET /sandboxes/{id}/settings/secrets/{name}` → raw secret value (called FROM sandbox)
#### Auth: `X-Session-API-Key` header, validated via `SandboxService.get_sandbox_by_session_api_key()`
#### Related SDK code (in `software-agent-sdk` repo):
- `openhands/sdk/llm/llm.py`: `LLM.api_key` accepts `SecretSource` (including `LookupSecret`)
- `openhands/workspace/cloud/workspace.py`: `get_llm()` and `get_secrets()` return LookupSecret-backed objects
- Tests: `tests/sdk/llm/test_llm_secret_source_api_key.py`, `tests/workspace/test_cloud_workspace_sdk_settings.py`

View File

@@ -1,82 +1,104 @@
# Contributing
Thanks for your interest in contributing to OpenHands! We welcome and appreciate contributions.
Thanks for your interest in contributing to OpenHands! We're building the future of AI-powered software development, and we'd love for you to be part of this journey.
## Understanding OpenHands's CodeBase
## Our Vision
To understand the codebase, please refer to the README in each module:
- [frontend](./frontend/README.md)
- [evaluation](./evaluation/README.md)
- [openhands](./openhands/README.md)
- [agenthub](./openhands/agenthub/README.md)
- [server](./openhands/server/README.md)
The OpenHands community is built around the belief that AI and AI agents are going to fundamentally change the way we build software. If this is true, we should do everything we can to make sure that the benefits provided by such powerful technology are accessible to everyone.
## Setting up Your Development Environment
We believe in the power of open source to democratize access to cutting-edge AI technology. Just as the internet transformed how we share information, we envision a world where AI-powered development tools are available to every developer, regardless of their background or resources.
We have a separate doc [Development.md](https://github.com/OpenHands/OpenHands/blob/main/Development.md) that tells
you how to set up a development workflow.
## Getting Started
## How Can I Contribute?
### Quick Ways to Contribute
There are many ways that you can contribute:
- **Use OpenHands** and [report issues](https://github.com/OpenHands/OpenHands/issues) you encounter
- **Give feedback** using the thumbs-up/thumbs-down buttons after each session
- **Star our repository** on [GitHub](https://github.com/OpenHands/OpenHands)
- **Share OpenHands** with other developers
1. **Download and use** OpenHands, and send [issues](https://github.com/OpenHands/OpenHands/issues) when you encounter something that isn't working or a feature that you'd like to see.
2. **Send feedback** after each session by [clicking the thumbs-up thumbs-down buttons](https://docs.openhands.dev/usage/feedback), so we can see where things are working and failing, and also build an open dataset for training code agents.
3. **Improve the Codebase** by sending [PRs](#sending-pull-requests-to-openhands) (see details below). In particular, we have some [good first issues](https://github.com/OpenHands/OpenHands/labels/good%20first%20issue) that may be ones to start on.
### Set Up Your Development Environment
## What Can I Build?
- **Requirements**: Linux/Mac/WSL, Docker, Python 3.12, Node.js 22+, Poetry 1.8+
- **Quick setup**: `make build`
- **Run locally**: `make run`
- **LLM setup (V1 web app)**: configure your model and API key in the Settings UI after the app starts
Here are a few ways you can help improve the codebase.
Full details in our [Development Guide](./Development.md).
#### UI/UX
### Find Your First Issue
We're always looking to improve the look and feel of the application. If you've got a small fix
for something that's bugging you, feel free to open up a PR that changes the [`./frontend`](./frontend) directory.
- Browse [good first issues](https://github.com/OpenHands/OpenHands/labels/good%20first%20issue)
- Check our [project boards](https://github.com/OpenHands/OpenHands/projects) for organized tasks
- Join our [Slack community](https://openhands.dev/joinslack) to ask what needs help
If you're looking to make a bigger change, add a new UI element, or significantly alter the style
of the application, please open an issue first, or better, join the #dev-ui-ux channel in our Slack
to gather consensus from our design team first.
## Understanding the Codebase
#### Improving the agent
- **[Frontend](./frontend/README.md)** - React application
- **[App Server (V1)](./openhands/app_server/README.md)** - Current FastAPI application server and REST API modules
- **[Runtime](./openhands/runtime/README.md)** - Execution environments
- **[Evaluation](https://github.com/OpenHands/benchmarks)** - Testing and benchmarks
Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/OpenHands/OpenHands/tree/main/openhands/agenthub/codeact_agent).
## What Can You Build?
Changes to these prompts, and to the underlying behavior in Python, can have a huge impact on user experience.
You can try modifying the prompts to see how they change the behavior of the agent as you use the app
locally, but we will need to do an end-to-end evaluation of any changes here to ensure that the agent
is getting better over time.
### Frontend & UI/UX
- React & TypeScript development
- UI/UX improvements
- Mobile responsiveness
- Component libraries
We use the [SWE-bench](https://www.swebench.com/) benchmark to test our agent. You can join the #evaluation
channel in Slack to learn more.
For bigger changes, join the #proj-gui channel in [Slack](https://openhands.dev/joinslack) first.
#### Adding a new agent
### Agent Development
- Prompt engineering
- New agent types
- Agent evaluation
- Multi-agent systems
You may want to experiment with building new types of agents. You can add an agent to [`openhands/agenthub`](./openhands/agenthub)
to help expand the capabilities of OpenHands.
We use [SWE-bench](https://www.swebench.com/) to evaluate agents.
#### Adding a new runtime
### Backend & Infrastructure
- Python development
- Runtime systems (Docker containers, sandboxes)
- Cloud integrations
- Performance optimization
The agent needs a place to run code and commands. When you run OpenHands on your laptop, it uses a Docker container
to do this by default. But there are other ways of creating a sandbox for the agent.
### Testing & Quality Assurance
- Unit testing
- Integration testing
- Bug hunting
- Performance testing
If you work for a company that provides a cloud-based runtime, you could help us add support for that runtime
by implementing the [interface specified here](https://github.com/OpenHands/OpenHands/blob/main/openhands/runtime/base.py).
### Documentation & Education
- Technical documentation
- Translation
- Community support
#### Testing
## Pull Request Process
When you write code, it is also good to write tests. Please navigate to the [`./tests`](./tests) folder to see existing
test suites. At the moment, we have these kinds of tests: [`unit`](./tests/unit), [`runtime`](./tests/runtime), and [`end-to-end (e2e)`](./tests/e2e).
Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure
quality of the project.
### Small Improvements
- Quick review and approval
- Ensure CI tests pass
- Include clear description of changes
### Core Agent Changes
These are evaluated based on:
- **Accuracy** - Does it make the agent better at solving problems?
- **Efficiency** - Does it improve speed or reduce resource usage?
- **Code Quality** - Is the code maintainable and well-tested?
Discuss major changes in [GitHub issues](https://github.com/OpenHands/OpenHands/issues) or [Slack](https://openhands.dev/joinslack) first.
## Sending Pull Requests to OpenHands
You'll need to fork our repository to send us a Pull Request. You can learn more
about how to fork a GitHub repo and open a PR with your changes in [this article](https://medium.com/swlh/forks-and-pull-requests-how-to-contribute-to-github-repos-8843fac34ce8).
### Pull Request title
You may also check out previous PRs in the [PR list](https://github.com/OpenHands/OpenHands/pulls).
As described [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), ideally a valid PR title should begin with one of the following prefixes:
### Pull Request Title Format
As described [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes:
- `feat`: A new feature
- `fix`: A bug fix
@@ -94,45 +116,27 @@ For example, a PR title could be:
- `refactor: modify package path`
- `feat(frontend): xxxx`, where `(frontend)` means that this PR mainly focuses on the frontend component.
You may also check out previous PRs in the [PR list](https://github.com/OpenHands/OpenHands/pulls).
### Pull Request Description
### Pull Request description
- Explain what the PR does and why
- Link to related issues
- Include screenshots for UI changes
- If your changes are user-facing (e.g. a new feature in the UI, a change in behavior, or a bugfix),
please include a short message that we can add to our changelog
- If your PR is small (such as a typo fix), you can go brief.
- If it contains a lot of changes, it's better to write more details.
## Becoming a Maintainer
If your changes are user-facing (e.g. a new feature in the UI, a change in behavior, or a bugfix)
please include a short message that we can add to our changelog.
For contributors who have made significant and sustained contributions to the project, there is a possibility of joining the maintainer team.
The process for this is as follows:
## How to Make Effective Contributions
1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated.
2. Once a maintainer nominates a new maintainer, there will be a discussion period among the maintainers for at least 3 days.
3. If no concerns are raised the nomination will be accepted by acclamation, and if concerns are raised there will be a discussion and possible vote.
### Opening Issues
Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md).
If you notice any bugs or have any feature requests please open them via the [issues page](https://github.com/OpenHands/OpenHands/issues). We will triage
based on how critical the bug is or how potentially useful the improvement is, discuss, and implement the ones that
the community has interest/effort for.
## Need Help?
Further, if you see an issue you like, please leave a "thumbs-up" or a comment, which will help us prioritize.
### Making Pull Requests
We're generally happy to consider all pull requests with the evaluation process varying based on the type of change:
#### For Small Improvements
Small improvements with few downsides are typically reviewed and approved quickly.
One thing to check when making changes is to ensure that all continuous integration tests pass, which you can check
before getting a review.
#### For Core Agent Changes
We need to be more careful with changes to the core agent, as it is imperative to maintain high quality. These PRs are
evaluated based on three key metrics:
1. **Accuracy**
2. **Efficiency**
3. **Code Complexity**
If it improves accuracy, efficiency, or both with only a minimal change to code quality, that's great we're happy to merge it in!
If there are bigger tradeoffs (e.g. helping efficiency a lot and hurting accuracy a little) we might want to put it behind a feature flag.
Either way, please feel free to discuss on github issues or slack, and we will give guidance and preliminary feedback.
- **Slack**: [Join our community](https://openhands.dev/joinslack)
- **GitHub Issues**: [Open an issue](https://github.com/OpenHands/OpenHands/issues)
- **Email**: contact@openhands.dev

View File

@@ -6,22 +6,196 @@ If you wish to contribute your changes, check out the
on how to clone and setup the project initially before moving on. Otherwise,
you can clone the OpenHands project directly.
## Start the Server for Development
## Choose Your Setup
### 1. Requirements
Select your operating system to see the specific setup instructions:
- Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [Ubuntu >= 22.04]
- [Docker](https://docs.docker.com/engine/install/) (For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
- [Python](https://www.python.org/downloads/) = 3.12
- [NodeJS](https://nodejs.org/en/download/package-manager) >= 22.x
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) >= 1.8
- OS-specific dependencies:
- Ubuntu: build-essential => `sudo apt-get install build-essential python3.12-dev`
- WSL: netcat => `sudo apt-get install netcat`
- [macOS](#macos-setup)
- [Linux](#linux-setup)
- [Windows WSL](#windows-wsl-setup)
- [Dev Container](#dev-container)
- [Developing in Docker](#developing-in-docker)
- [No sudo access?](#develop-without-sudo-access)
Make sure you have all these dependencies installed before moving on to `make build`.
---
#### Dev container
## macOS Setup
### 1. Install Prerequisites
You'll need the following installed:
- **Python 3.12** — `brew install python@3.12` (see the [official Homebrew Python docs](https://docs.brew.sh/Homebrew-and-Python) for details). Make sure `python3.12` is available in your PATH (the `make build` step will verify this).
- **Node.js >= 22** — `brew install node`
- **Poetry >= 1.8** — `brew install poetry`
- **Docker Desktop** — `brew install --cask docker`
- After installing, open Docker Desktop → **Settings → Advanced** → Enable **"Allow the default Docker socket to be used"**
### 2. Build and Setup the Environment
```bash
make build
```
### 3. Configure the Language Model
OpenHands supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library.
For the V1 web app, start OpenHands and configure your model and API key in the Settings UI.
If you are running headless or CLI workflows, you can prepare local defaults with:
```bash
make setup-config
```
**Note on Alternative Models:**
See [our documentation](https://docs.openhands.dev/usage/llms) for recommended models.
### 4. Run the Application
```bash
# Run both backend and frontend
make run
# Or run separately:
make start-backend # Backend only on port 3000
make start-frontend # Frontend only on port 3001
```
These targets serve the current OpenHands V1 API by default. In the codebase, `make start-backend` runs `openhands.server.listen:app`, and that app includes the `openhands/app_server` V1 routes unless `ENABLE_V1=0`.
---
## Linux Setup
This guide covers Ubuntu/Debian. For other distributions, adapt the package manager commands accordingly.
### 1. Install Prerequisites
```bash
# Update package list
sudo apt update
# Install system dependencies
sudo apt install -y build-essential curl netcat software-properties-common
# Install Python 3.12
# Ubuntu 24.04+ and Debian 13+ ship with Python 3.12 — skip the PPA step if
# python3.12 --version already works on your system.
# The deadsnakes PPA is Ubuntu-only and needed for Ubuntu 22.04 or older:
sudo add-apt-repository -y ppa:deadsnakes/ppa
sudo apt update
sudo apt install -y python3.12 python3.12-dev python3.12-venv
# Install Node.js 22.x
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
# Install Poetry
curl -sSL https://install.python-poetry.org | python3 -
# Add Poetry to your PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Install Docker
# Follow the official guide: https://docs.docker.com/engine/install/ubuntu/
# Quick version:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
# Log out and back in for Docker group changes to take effect
```
### 2. Build and Setup the Environment
```bash
make build
```
### 3. Configure the Language Model
See the [macOS section above](#3-configure-the-language-model) for guidance: configure your model and API key in the Settings UI.
### 4. Run the Application
```bash
# Run both backend and frontend
make run
# Or run separately:
make start-backend # Backend only on port 3000
make start-frontend # Frontend only on port 3001
```
---
## Windows WSL Setup
WSL2 with Ubuntu is recommended. The setup is similar to Linux, with a few WSL-specific considerations.
### 1. Install WSL2
**Option A: Windows 11 (Microsoft Store)**
The easiest way on Windows 11:
1. Open the **Microsoft Store** app
2. Search for **"Ubuntu 22.04 LTS"** or **"Ubuntu"**
3. Click **Install**
4. Launch Ubuntu from the Start menu
**Option B: PowerShell**
```powershell
# Run this in PowerShell as Administrator
wsl --install -d Ubuntu-22.04
```
After installation, restart your computer and open Ubuntu.
### 2. Install Prerequisites (in WSL Ubuntu)
Follow [Step 1 from the Linux setup](#1-install-prerequisites-1) to install system dependencies, Python 3.12, Node.js, and Poetry. Skip the Docker installation — Docker is provided through Docker Desktop below.
### 3. Configure Docker for WSL2
1. Install [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop)
2. Open Docker Desktop > Settings > General
3. Enable: "Use the WSL 2 based engine"
4. Go to Settings > Resources > WSL Integration
5. Enable integration with your Ubuntu distribution
**Important:** Keep your project files in the WSL filesystem (e.g., `~/workspace/openhands`), not in `/mnt/c`. Files accessed via `/mnt/c` will be significantly slower.
### 4. Build and Setup the Environment
```bash
make build
```
### 5. Configure the Language Model
See the [macOS section above](#3-configure-the-language-model) for the current V1 guidance: configure your model and API key in the Settings UI for the web app, and use `make setup-config` only for headless or CLI workflows.
### 6. Run the Application
```bash
# Run both backend and frontend
make run
# Or run separately:
make start-backend # Backend only on port 3000
make start-frontend # Frontend only on port 3001
```
Access the frontend at `http://localhost:3001` from your Windows browser.
---
## Dev Container
There is a [dev container](https://containers.dev/) available which provides a
pre-configured environment with all the necessary dependencies installed if you
@@ -32,7 +206,38 @@ extension installed, you can open the project in a dev container by using the
_Dev Container: Reopen in Container_ command from the Command Palette
(Ctrl+Shift+P).
#### Develop without sudo access
---
## Developing in Docker
If you don't want to install dependencies on your host machine, you can develop inside a Docker container.
### Quick Start
```bash
make docker-dev
```
For more details, see the [dev container documentation](./containers/dev/README.md).
### Alternative: Docker Run
If you just want to run OpenHands without setting up a dev environment:
```bash
make docker-run
```
If you don't have `make` installed, run:
```bash
cd ./containers/dev
./dev.sh
```
---
## Develop without sudo access
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJS`, you can use
`conda` or `mamba` to manage the packages for you:
@@ -48,159 +253,90 @@ mamba install conda-forge::nodejs
mamba install conda-forge::poetry
```
### 2. Build and Setup The Environment
---
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
that OpenHands is ready to run on your system:
## Running OpenHands with OpenHands
You can use OpenHands to develop and improve OpenHands itself!
### Quick Start
```bash
make build
export INSTALL_DOCKER=0
export RUNTIME=local
make build && make run
```
### 3. Configuring the Language Model
Access the interface at:
- Local development: http://localhost:3001
- Remote/cloud environments: Use the appropriate external URL
OpenHands supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library.
For external access:
```bash
make run FRONTEND_PORT=12000 FRONTEND_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0
```
To configure the LM of your choice, run:
---
## LLM Debugging
If you encounter issues with the Language Model, enable debug logging:
```bash
make setup-config
export DEBUG=1
# Restart the backend
make start-backend
```
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
please set the model in the UI.
Logs will be saved to `logs/llm/CURRENT_DATE/` for troubleshooting.
Note: If you have previously run OpenHands using the docker command, you may have already set some environment
variables in your terminal. The final configurations are set from highest to lowest priority:
Environment variables > config.toml variables > default variables
---
**Note on Alternative Models:**
See [our documentation](https://docs.openhands.dev/usage/llms) for recommended models.
## Testing
### 4. Running the application
#### Option A: Run the Full Application
Once the setup is complete, this command starts both the backend and frontend servers, allowing you to interact with OpenHands:
```bash
make run
```
#### Option B: Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
components or interface enhancements.
```bash
make start-frontend
```
### 5. Running OpenHands with OpenHands
You can use OpenHands to develop and improve OpenHands itself! This is a powerful way to leverage AI assistance for contributing to the project.
#### Quick Start
1. **Build and run OpenHands:**
```bash
export INSTALL_DOCKER=0
export RUNTIME=local
make build && make run
```
2. **Access the interface:**
- Local development: http://localhost:3001
- Remote/cloud environments: Use the appropriate external URL
3. **Configure for external access (if needed):**
```bash
# For external access (e.g., cloud environments)
make run FRONTEND_PORT=12000 FRONTEND_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0
```
### 6. LLM Debugging
If you encounter any issues with the Language Model (LM) or you're simply curious, export DEBUG=1 in the environment and restart the backend.
OpenHands will log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
### 7. Help
Need help or info on available targets and commands? Use the help command for all the guidance you need with OpenHands.
```bash
make help
```
### 8. Testing
To run tests, refer to the following:
#### Unit tests
### Unit Tests
```bash
poetry run pytest ./tests/unit/test_*.py
```
### 9. Add or update dependency
---
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`.
2. Update the poetry.lock file via `poetry lock --no-update`.
## Adding Dependencies
### 10. Use existing Docker image
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`
2. Update the lock file: `poetry lock --no-update`
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
---
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:1.2-nikolaik`
## Using Existing Docker Images
## Develop inside Docker container
TL;DR
To reduce build time, you can use an existing runtime image:
```bash
make docker-dev
export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:1.2-nikolaik
```
See more details [here](./containers/dev/README.md).
---
If you are just interested in running `OpenHands` without installing all the required tools on your host.
## Help
```bash
make docker-run
make help
```
If you do not have `make` on your host, run:
```bash
cd ./containers/dev
./dev.sh
```
You do need [Docker](https://docs.docker.com/engine/install/) installed on your host though.
---
## Key Documentation Resources
Here's a guide to the important documentation files in the repository:
- [/README.md](./README.md): Main project overview, features, and basic setup instructions
- [/Development.md](./Development.md) (this file): Comprehensive guide for developers working on OpenHands
- [/CONTRIBUTING.md](./CONTRIBUTING.md): Guidelines for contributing to the project, including code style and PR process
- [DOC_STYLE_GUIDE.md](https://github.com/OpenHands/docs/blob/main/openhands/DOC_STYLE_GUIDE.md): Standards for writing and maintaining project documentation
- [/openhands/README.md](./openhands/README.md): Details about the backend Python implementation
- [/openhands/app_server/README.md](./openhands/app_server/README.md): Current V1 application server implementation and REST API modules
- [/frontend/README.md](./frontend/README.md): Frontend React application setup and development guide
- [/containers/README.md](./containers/README.md): Information about Docker containers and deployment
- [/tests/unit/README.md](./tests/unit/README.md): Guide to writing and running unit tests
- [/evaluation/README.md](./evaluation/README.md): Documentation for the evaluation framework and benchmarks
- [OpenHands/benchmarks](https://github.com/OpenHands/benchmarks): Documentation for the evaluation framework and benchmarks
- [/skills/README.md](./skills/README.md): Information about the skills architecture and implementation
- [/openhands/server/README.md](./openhands/server/README.md): Server implementation details and API documentation
- [/openhands/runtime/README.md](./openhands/runtime/README.md): Documentation for the runtime environment and execution model

View File

@@ -11,7 +11,15 @@ DEFAULT_WORKSPACE_DIR = "./workspace"
DEFAULT_MODEL = "gpt-4o"
CONFIG_FILE = config.toml
PRE_COMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
PYTHON_VERSION = 3.12
PYTHON_MIN_VERSION = 3.12
PYTHON_MAX_VERSION = 3.14
PYTHON_CANDIDATES ?= python3.13 python3.12 python3
PYTHON ?= $(shell for cmd in $(PYTHON_CANDIDATES); do \
if command -v $$cmd > /dev/null 2>&1 && $$cmd -c 'import sys; raise SystemExit(0 if ((3, 12) <= sys.version_info[:2] < (3, 14)) else 1)' > /dev/null 2>&1; then \
echo $$cmd; \
exit 0; \
fi; \
done)
KIND_CLUSTER_NAME = "local-hands"
# ANSI color codes
@@ -63,10 +71,10 @@ check-system:
check-python:
@echo "$(YELLOW)Checking Python installation...$(RESET)"
@if command -v python$(PYTHON_VERSION) > /dev/null; then \
echo "$(BLUE)$(shell python$(PYTHON_VERSION) --version) is already installed.$(RESET)"; \
@if [ -n "$(PYTHON)" ]; then \
echo "$(BLUE)$$($(PYTHON) --version) is already installed (using $(PYTHON)).$(RESET)"; \
else \
echo "$(RED)Python $(PYTHON_VERSION) is not installed. Please install Python $(PYTHON_VERSION) to continue.$(RESET)"; \
echo "$(RED)A compatible Python interpreter (>= $(PYTHON_MIN_VERSION), < $(PYTHON_MAX_VERSION)) is required. Please install Python 3.12 or 3.13 to continue.$(RESET)"; \
exit 1; \
fi
@@ -118,31 +126,34 @@ check-tmux:
check-poetry:
@echo "$(YELLOW)Checking Poetry installation...$(RESET)"
@if command -v poetry > /dev/null; then \
@if [ -z "$(PYTHON)" ]; then \
echo "$(RED)A compatible Python interpreter (>= $(PYTHON_MIN_VERSION), < $(PYTHON_MAX_VERSION)) is required. Please install Python 3.12 or 3.13 to continue.$(RESET)"; \
exit 1; \
elif command -v poetry > /dev/null; then \
POETRY_VERSION=$(shell poetry --version 2>&1 | sed -E 's/Poetry \(version ([0-9]+\.[0-9]+\.[0-9]+)\)/\1/'); \
IFS='.' read -r -a POETRY_VERSION_ARRAY <<< "$$POETRY_VERSION"; \
if [ $${POETRY_VERSION_ARRAY[0]} -gt 1 ] || ([ $${POETRY_VERSION_ARRAY[0]} -eq 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]); then \
echo "$(BLUE)$(shell poetry --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Poetry 1.8 or later is required. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python$(PYTHON_VERSION) -$(RESET)"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | $(PYTHON) -$(RESET)"; \
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
exit 1; \
fi; \
else \
echo "$(RED)Poetry is not installed. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | python$(PYTHON_VERSION) -$(RESET)"; \
echo "$(RED) curl -sSL https://install.python-poetry.org | $(PYTHON) -$(RESET)"; \
echo "$(RED)More detail here: https://python-poetry.org/docs/#installing-with-the-official-installer$(RESET)"; \
exit 1; \
fi
install-python-dependencies:
install-python-dependencies: check-python
@echo "$(GREEN)Installing Python dependencies...$(RESET)"
@if [ -z "${TZ}" ]; then \
echo "Defaulting TZ (timezone) to UTC"; \
export TZ="UTC"; \
fi
poetry env use python$(PYTHON_VERSION)
poetry env use $(PYTHON)
@if [ "$(shell uname)" = "Darwin" ]; then \
echo "$(BLUE)Installing chroma-hnswlib...$(RESET)"; \
export HNSWLIB_NO_NATIVE=1; \

View File

@@ -23,7 +23,6 @@
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=pt">Português</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=ru">Русский</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=zh">中文</a>
</div>
<hr>
@@ -54,7 +53,7 @@ The experience will be familiar to anyone who has used Devin or Jules.
### OpenHands Cloud
This is a deployment of OpenHands GUI, running on hosted infrastructure.
You can try it with a free $10 credit by [signing in with your GitHub or GitLab account](https://app.all-hands.dev).
You can try it for free using the Minimax model by [signing in with your GitHub or GitLab account](https://app.all-hands.dev).
OpenHands Cloud comes with source-available features and integrations:
- Integrations with Slack, Jira, and Linear
@@ -84,3 +83,71 @@ All our work is available under the MIT license, except for the `enterprise/` di
The core `openhands` and `agent-server` Docker images are fully MIT-licensed as well.
If you need help with anything, or just want to chat, [come find us on Slack](https://dub.sh/openhands).
<hr>
### Thank You to Our Contributors
<div align="center">
[![OpenHands Contributors](https://assets.openhands.dev/readme/openhands-openhands-contributors.svg)](https://github.com/OpenHands/OpenHands/graphs/contributors)
</div>
<hr>
### Trusted by Engineers at
<div align="center">
<br/><br/>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/tiktok.svg">
<img src="https://assets.openhands.dev/logos/external/black/tiktok.svg" alt="TikTok" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/vmware.svg">
<img src="https://assets.openhands.dev/logos/external/black/vmware.svg" alt="VMware" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/roche.svg">
<img src="https://assets.openhands.dev/logos/external/black/roche.svg" alt="Roche" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/amazon.svg">
<img src="https://assets.openhands.dev/logos/external/black/amazon.svg" alt="Amazon" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/c3-ai.svg">
<img src="https://assets.openhands.dev/logos/external/black/c3-ai.svg" alt="C3 AI" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/netflix.svg">
<img src="https://assets.openhands.dev/logos/external/black/netflix.svg" alt="Netflix" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/mastercard.svg">
<img src="https://assets.openhands.dev/logos/external/black/mastercard.svg" alt="Mastercard" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/red-hat.svg">
<img src="https://assets.openhands.dev/logos/external/black/red-hat.svg" alt="Red Hat" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/mongodb.svg">
<img src="https://assets.openhands.dev/logos/external/black/mongodb.svg" alt="MongoDB" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/apple.svg">
<img src="https://assets.openhands.dev/logos/external/black/apple.svg" alt="Apple" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/nvidia.svg">
<img src="https://assets.openhands.dev/logos/external/black/nvidia.svg" alt="NVIDIA" height="17" hspace="5">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://assets.openhands.dev/logos/external/white/google.svg">
<img src="https://assets.openhands.dev/logos/external/black/google.svg" alt="Google" height="17" hspace="5">
</picture>
</div>
</div>

View File

@@ -296,7 +296,7 @@ classpath = "my_package.my_module.MyCustomAgent"
#user_id = 1000
# Container image to use for the sandbox
#base_container_image = "nikolaik/python-nodejs:python3.12-nodejs22"
#base_container_image = "nikolaik/python-nodejs:python3.12-nodejs22-slim"
# Use host network
#use_host_network = false
@@ -440,12 +440,6 @@ type = "noop"
#temperature = 0.1
#max_input_tokens = 1024
#################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation
# plugin for the available options
##############################################################################
########################### Kubernetes #######################################
# Kubernetes configuration when using the Kubernetes runtime
##############################################################################

View File

@@ -1,5 +1,5 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:25.2-trixie-slim AS frontend-builder
FROM node:25.9-trixie-slim AS frontend-builder
WORKDIR /app
@@ -20,9 +20,11 @@ ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
# Pin Poetry version to match the version used to generate poetry.lock
ARG POETRY_VERSION=2.3.3
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential jq gettext \
&& python3 -m pip install poetry --break-system-packages
&& python3 -m pip install "poetry==${POETRY_VERSION}" --break-system-packages
COPY pyproject.toml poetry.lock ./
RUN touch README.md
@@ -50,7 +52,7 @@ RUN mkdir -p $FILE_STORE_PATH
RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \
&& apt-get install -y curl ssh sudo \
&& apt-get install -y curl git ssh sudo \
&& rm -rf /var/lib/apt/lists/*
# Default is 1000, but OSX is often 501
@@ -73,13 +75,22 @@ ENV VIRTUAL_ENV=/app/.venv \
COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# Pin pip to a known-good version (reproducible builds) and fix CVE-2025-8869
# Pin both venv pip and system pip (Trivy scans both)
# - `python -m pip` uses the venv because `PATH` is prefixed with `${VIRTUAL_ENV}/bin`
# - `/usr/local/bin/python3 -m pip` uses the system interpreter regardless of `PATH`
ARG PIP_VERSION=26.0.1
RUN python -m pip install --no-cache-dir "pip==${PIP_VERSION}"
USER root
RUN /usr/local/bin/python3 -m pip install --no-cache-dir "pip==${PIP_VERSION}" --break-system-packages
USER openhands
COPY --chown=openhands:openhands --chmod=770 ./skills ./skills
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:openhands
RUN find /app \! -group openhands -exec chgrp openhands {} +

View File

@@ -1,4 +0,0 @@
DOCKER_REGISTRY=ghcr.io
DOCKER_ORG=openhands
DOCKER_IMAGE=openhands
DOCKER_BASE_DIR="."

View File

@@ -1,182 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
# Initialize variables with default values
image_name=""
org_name=""
push=0
load=0
tag_suffix=""
dry_run=0
# Function to display usage information
usage() {
echo "Usage: $0 -i <image_name> [-o <org_name>] [--push] [--load] [-t <tag_suffix>] [--dry]"
echo " -i: Image name (required)"
echo " -o: Organization name"
echo " --push: Push the image"
echo " --load: Load the image"
echo " -t: Tag suffix"
echo " --dry: Don't build, only create build-args.json"
exit 1
}
# Parse command-line options
while [[ $# -gt 0 ]]; do
case $1 in
-i) image_name="$2"; shift 2 ;;
-o) org_name="$2"; shift 2 ;;
--push) push=1; shift ;;
--load) load=1; shift ;;
-t) tag_suffix="$2"; shift 2 ;;
--dry) dry_run=1; shift ;;
*) usage ;;
esac
done
# Check if required arguments are provided
if [[ -z "$image_name" ]]; then
echo "Error: Image name is required."
usage
fi
echo "Building: $image_name"
tags=()
OPENHANDS_BUILD_VERSION="dev"
cache_tag_base="buildcache"
cache_tag="$cache_tag_base"
if [[ -n $RELEVANT_SHA ]]; then
git_hash=$(git rev-parse --short "$RELEVANT_SHA")
tags+=("$git_hash")
tags+=("$RELEVANT_SHA")
fi
if [[ -n $GITHUB_REF_NAME ]]; then
# check if ref name is a version number
if [[ $GITHUB_REF_NAME =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
major_version=$(echo "$GITHUB_REF_NAME" | cut -d. -f1)
minor_version=$(echo "$GITHUB_REF_NAME" | cut -d. -f1,2)
tags+=("$major_version" "$minor_version")
tags+=("latest")
fi
sanitized_ref_name=$(echo "$GITHUB_REF_NAME" | sed 's/[^a-zA-Z0-9.-]\+/-/g')
OPENHANDS_BUILD_VERSION=$sanitized_ref_name
sanitized_ref_name=$(echo "$sanitized_ref_name" | tr '[:upper:]' '[:lower:]') # lower case is required in tagging
tags+=("$sanitized_ref_name")
cache_tag+="-${sanitized_ref_name}"
fi
if [[ -n $tag_suffix ]]; then
cache_tag+="-${tag_suffix}"
for i in "${!tags[@]}"; do
tags[$i]="${tags[$i]}-$tag_suffix"
done
fi
echo "Tags: ${tags[@]}"
if [[ "$image_name" == "openhands" ]]; then
dir="./containers/app"
elif [[ "$image_name" == "runtime" ]]; then
dir="./containers/runtime"
else
dir="./containers/$image_name"
fi
if [[ (! -f "$dir/Dockerfile") && "$image_name" != "runtime" ]]; then
# Allow runtime to be built without a Dockerfile
echo "No Dockerfile found"
exit 1
fi
if [[ ! -f "$dir/config.sh" ]]; then
echo "No config.sh found for Dockerfile"
exit 1
fi
source "$dir/config.sh"
if [[ -n "$org_name" ]]; then
DOCKER_ORG="$org_name"
fi
# If $DOCKER_IMAGE_SOURCE_TAG is set, add it to the tags
if [[ -n "$DOCKER_IMAGE_SOURCE_TAG" ]]; then
tags+=("$DOCKER_IMAGE_SOURCE_TAG")
fi
# If $DOCKER_IMAGE_TAG is set, add it to the tags
if [[ -n "$DOCKER_IMAGE_TAG" ]]; then
tags+=("$DOCKER_IMAGE_TAG")
fi
DOCKER_REPOSITORY="$DOCKER_REGISTRY/$DOCKER_ORG/$DOCKER_IMAGE"
DOCKER_REPOSITORY=${DOCKER_REPOSITORY,,} # lowercase
echo "Repo: $DOCKER_REPOSITORY"
echo "Base dir: $DOCKER_BASE_DIR"
args=""
full_tags=()
for tag in "${tags[@]}"; do
args+=" -t $DOCKER_REPOSITORY:$tag"
full_tags+=("$DOCKER_REPOSITORY:$tag")
done
if [[ $push -eq 1 ]]; then
args+=" --push"
args+=" --cache-to=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag,mode=max"
fi
if [[ $load -eq 1 ]]; then
args+=" --load"
fi
echo "Args: $args"
# Modify the platform selection based on --load flag
if [[ $load -eq 1 ]]; then
# When loading, build only for the current platform
platform=$(docker version -f '{{.Server.Os}}/{{.Server.Arch}}')
else
# For push or without load, build for multiple platforms
platform="linux/amd64,linux/arm64"
fi
if [[ $dry_run -eq 1 ]]; then
echo "Dry Run is enabled. Writing build config to docker-build-dry.json"
jq -n \
--argjson tags "$(printf '%s\n' "${full_tags[@]}" | jq -R . | jq -s .)" \
--arg platform "$platform" \
--arg openhands_build_version "$OPENHANDS_BUILD_VERSION" \
--arg dockerfile "$dir/Dockerfile" \
'{
tags: $tags,
platform: $platform,
build_args: [
"OPENHANDS_BUILD_VERSION=" + $openhands_build_version
],
dockerfile: $dockerfile
}' > docker-build-dry.json
exit 0
fi
echo "Building for platform(s): $platform"
docker buildx build \
$args \
--build-arg OPENHANDS_BUILD_VERSION="$OPENHANDS_BUILD_VERSION" \
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag \
--cache-from=type=registry,ref=$DOCKER_REPOSITORY:$cache_tag_base-main \
--platform $platform \
--provenance=false \
-f "$dir/Dockerfile" \
"$DOCKER_BASE_DIR"
# If load was requested, print the loaded images
if [[ $load -eq 1 ]]; then
echo "Local images built:"
docker images "$DOCKER_REPOSITORY" --format "{{.Repository}}:{{.Tag}}"
fi

View File

@@ -12,8 +12,8 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/runtime}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-1.2-nikolaik}
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/agent-server}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-1.15.0-python}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -1,12 +0,0 @@
# Dynamically constructed Dockerfile
This folder builds a runtime image (sandbox), which will use a dynamically generated `Dockerfile`
that depends on the `base_image` **AND** a [Python source distribution](https://docs.python.org/3.10/distutils/sourcedist.html) that is based on the current commit of `openhands`.
The following command will generate a `Dockerfile` file for `nikolaik/python-nodejs:python3.12-nodejs22` (the default base image), an updated `config.sh` and the runtime source distribution files/folders into `containers/runtime`:
```bash
poetry run python3 -m openhands.runtime.utils.runtime_build \
--base_image nikolaik/python-nodejs:python3.12-nodejs22 \
--build_folder containers/runtime
```

View File

@@ -1,7 +0,0 @@
DOCKER_REGISTRY=ghcr.io
DOCKER_ORG=openhands
DOCKER_BASE_DIR="./containers/runtime"
DOCKER_IMAGE=runtime
# These variables will be appended by the runtime_build.py script
# DOCKER_IMAGE_TAG=
# DOCKER_IMAGE_SOURCE_TAG=

View File

@@ -58,6 +58,8 @@ repos:
types-Markdown,
pydantic,
lxml,
"openhands-sdk==1.17.0",
"openhands-tools==1.17.0",
]
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file dev_config/python/mypy.ini openhands/

View File

@@ -14,3 +14,11 @@ exclude = (third_party/|enterprise/)
[mypy-openhands.memory.condenser.impl.*]
disable_error_code = override
[mypy-openai.*]
follow_imports = skip
ignore_missing_imports = True
[mypy-litellm.*]
follow_imports = skip
ignore_missing_imports = True

View File

@@ -8,7 +8,7 @@ services:
container_name: openhands-app-${DATE:-}
environment:
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/agent-server}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-31536c8-python}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-1.15.0-python}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -10,7 +10,7 @@ LABEL com.datadoghq.tags.env="${DD_ENV}"
# Apply security updates to fix CVEs
RUN apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
apt-get install -y nodejs && \
apt-get install -y jq gettext && \
# Apply security updates for packages with available fixes
@@ -23,12 +23,23 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Python packages with security fixes
RUN /app/.venv/bin/pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace "posthog>=6.0.0" "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy google-cloud-recaptcha-enterprise && \
# Update packages with known CVE fixes
/app/.venv/bin/pip install --upgrade \
"mcp>=1.10.0" \
"pillow>=11.3.0"
# Install poetry and export before importing current code.
RUN /app/.venv/bin/pip install poetry poetry-plugin-export
# Install Python dependencies from poetry.lock for reproducible builds
# Copy lock files first for better Docker layer caching
COPY --chown=openhands:openhands enterprise/pyproject.toml enterprise/poetry.lock /tmp/enterprise/
RUN cd /tmp/enterprise && \
# Export only main dependencies with hashes for supply chain security
/app/.venv/bin/poetry export --only main -o requirements.txt && \
# Remove the local path dependency (openhands-ai is already in base image)
sed -i '/^-e /d; /openhands-ai/d' requirements.txt && \
# Install pinned dependencies from lock file
/app/.venv/bin/pip install -r requirements.txt && \
# Cleanup - return to /app before removing /tmp/enterprise
cd /app && \
rm -rf /tmp/enterprise && \
/app/.venv/bin/pip uninstall -y poetry poetry-plugin-export
WORKDIR /app
COPY --chown=openhands:openhands --chmod=770 enterprise .

View File

@@ -1,5 +1,7 @@
# PolyForm Free Trial License 1.0.0
Copyright (c) 2026 All Hands AI
## Acceptance
In order to get any license under these terms, you must agree

View File

@@ -51,6 +51,6 @@ NOTE: in the future we will simply replace the `GithubTokenManager` with keycloa
## User ID vs User Token
- In OpenHands, the entire app revolves around the GitHub token the user sets. `openhands/server` uses `request.state.github_token` for the entire app
- On Enterprise, the entire APP resolves around the Github User ID. This is because the cookie sets it, so `openhands/server` AND `enterprise/server` depend on it and completly ignore `request.state.github_token` (token is fetched from `GithubTokenManager` instead)
- On Enterprise, the entire APP resolves around the Github User ID. This is because the cookie sets it, so `openhands/server` AND `enterprise/server` depend on it and completely ignore `request.state.github_token` (token is fetched from `GithubTokenManager` instead)
Note that introducing GitHub User ID in OpenHands, for instance, will cause large breakages.

View File

@@ -59,7 +59,7 @@ handlers = console
qualname =
[logger_sqlalchemy]
level = DEBUG
level = WARNING
handlers =
qualname = sqlalchemy.engine

View File

@@ -723,11 +723,15 @@
"https://$WEB_HOST/slack/keycloak-callback",
"https://$WEB_HOST/oauth/device/keycloak-callback",
"https://$WEB_HOST/api/email/verified",
"/realms/$KEYCLOAK_REALM_NAME/$KEYCLOAK_CLIENT_ID/*"
"/realms/$KEYCLOAK_REALM_NAME/$KEYCLOAK_CLIENT_ID/*",
"https://laminar.$WEB_HOST/api/auth/callback/keycloak",
"https://analytics.$WEB_HOST/api/auth/callback/keycloak"
],
"webOrigins": [
"https://$WEB_HOST",
"https://$AUTH_WEB_HOST"
"https://$AUTH_WEB_HOST",
"https://laminar.$WEB_HOST",
"https://analytics.$WEB_HOST"
],
"notBefore": 0,
"bearerOnly": false,
@@ -1772,6 +1776,40 @@
"sendIdTokenOnLogout": "true",
"passMaxAge": "false"
}
},
{
"alias": "bitbucket_data_center",
"displayName": "Bitbucket Data Center",
"internalId": "b77b4ead-20e8-451c-ad27-99f92d561616",
"providerId": "oauth2",
"enabled": true,
"updateProfileFirstLoginMode": "on",
"trustEmail": true,
"storeToken": true,
"addReadTokenRoleOnCreate": false,
"authenticateByDefault": false,
"linkOnly": false,
"hideOnLogin": false,
"config": {
"givenNameClaim": "given_name",
"userInfoUrl": "https://${WEB_HOST}/bitbucket-dc-proxy/oauth2/userinfo",
"clientId": "$BITBUCKET_DATA_CENTER_CLIENT_ID",
"tokenUrl": "https://${BITBUCKET_DATA_CENTER_HOST}/rest/oauth2/latest/token",
"acceptsPromptNoneForwardFromClient": "false",
"fullNameClaim": "name",
"userIDClaim": "sub",
"emailClaim": "email",
"userNameClaim": "preferred_username",
"caseSensitiveOriginalUsername": "false",
"familyNameClaim": "family_name",
"pkceEnabled": "false",
"authorizationUrl": "https://${BITBUCKET_DATA_CENTER_HOST}/rest/oauth2/latest/authorize",
"clientAuthMethod": "client_secret_post",
"syncMode": "IMPORT",
"clientSecret": "$BITBUCKET_DATA_CENTER_CLIENT_SECRET",
"allowedClockSkew": "0",
"defaultScope": "REPO_WRITE"
}
}
],
"identityProviderMappers": [
@@ -1829,6 +1867,26 @@
"syncMode": "FORCE",
"attribute": "identity_provider"
}
},
{
"name": "id-mapper",
"identityProviderAlias": "bitbucket_data_center",
"identityProviderMapper": "oidc-user-attribute-idp-mapper",
"config": {
"syncMode": "FORCE",
"claim": "sub",
"user.attribute": "bitbucket_data_center_id"
}
},
{
"name": "identity-provider",
"identityProviderAlias": "bitbucket_data_center",
"identityProviderMapper": "hardcoded-attribute-idp-mapper",
"config": {
"attribute.value": "bitbucket_data_center",
"syncMode": "FORCE",
"attribute": "identity_provider"
}
}
],
"components": {

View File

@@ -50,8 +50,10 @@ repos:
- ./
- stripe==11.5.0
- pygithub==2.6.1
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file enterprise/dev_config/python/mypy.ini enterprise/
# Use -p (package) to avoid dual module name conflict when using MYPYPATH
# MYPYPATH=enterprise allows resolving bare imports like "from integrations.xxx"
# Note: tests package excluded to avoid conflict with core openhands tests
entry: bash -c 'MYPYPATH=enterprise mypy --config-file enterprise/dev_config/python/mypy.ini -p integrations -p server -p storage -p sync'
always_run: true
pass_filenames: false
files: ^enterprise/

View File

@@ -2,7 +2,6 @@
warn_unused_configs = True
ignore_missing_imports = True
check_untyped_defs = True
explicit_package_bases = True
warn_unreachable = True
warn_redundant_casts = True
no_implicit_optional = True

View File

@@ -0,0 +1,13 @@
# Enterprise Architecture Documentation
Architecture diagrams specific to the OpenHands SaaS/Enterprise deployment.
## Documentation
- [Authentication Flow](./authentication.md) - Keycloak-based authentication for SaaS deployment
- [External Integrations](./external-integrations.md) - GitHub, Slack, Jira, and other service integrations
## Related Documentation
For core OpenHands architecture (applicable to all deployments), see:
- [Core Architecture Documentation](../../../openhands/architecture/README.md)

View File

@@ -0,0 +1,58 @@
# Authentication Flow (SaaS Deployment)
OpenHands uses Keycloak for identity management in the SaaS deployment. The authentication flow involves multiple services:
```mermaid
sequenceDiagram
autonumber
participant User as User (Browser)
participant App as App Server
participant KC as Keycloak
participant IdP as Identity Provider<br/>(GitHub, Google, etc.)
participant DB as User Database
Note over User,DB: OAuth 2.0 / OIDC Authentication Flow
User->>App: Access OpenHands
App->>User: Redirect to Keycloak
User->>KC: Login request
KC->>User: Show login options
User->>KC: Select provider (e.g., GitHub)
KC->>IdP: OAuth redirect
User->>IdP: Authenticate
IdP-->>KC: OAuth callback + tokens
Note over KC: Create/update user session
KC-->>User: Redirect with auth code
User->>App: Auth code
App->>KC: Exchange code for tokens
KC-->>App: Access token + Refresh token
Note over App: Create signed JWT cookie
App->>DB: Store/update user record
App-->>User: Set keycloak_auth cookie
Note over User,DB: Subsequent Requests
User->>App: Request with cookie
Note over App: Verify JWT signature
App->>KC: Validate token (if needed)
KC-->>App: Token valid
Note over App: Extract user context
App-->>User: Authorized response
```
### Authentication Components
| Component | Purpose | Location |
|-----------|---------|----------|
| **Keycloak** | Identity provider, SSO, token management | External service |
| **UserAuth** | Abstract auth interface | `openhands/server/user_auth/user_auth.py` |
| **SaasUserAuth** | Keycloak implementation | `enterprise/server/auth/saas_user_auth.py` |
| **JWT Service** | Token signing/verification | `openhands/app_server/services/jwt_service.py` |
| **Auth Routes** | Login/logout endpoints | `enterprise/server/routes/auth.py` |
### Token Flow
1. **Keycloak Access Token**: Short-lived token for API access
2. **Keycloak Refresh Token**: Long-lived token to obtain new access tokens
3. **Signed JWT Cookie**: App Server's session cookie containing encrypted Keycloak tokens
4. **Provider Tokens**: OAuth tokens for GitHub, GitLab, etc. (stored separately for git operations)

View File

@@ -0,0 +1,88 @@
# External Integrations
OpenHands integrates with external services (GitHub, Slack, Jira, etc.) through webhook-based event handling:
```mermaid
sequenceDiagram
autonumber
participant Ext as External Service<br/>(GitHub/Slack/Jira)
participant App as App Server
participant IntRouter as Integration Router
participant Manager as Integration Manager
participant Conv as Conversation Service
participant Sandbox as Sandbox
Note over Ext,Sandbox: Webhook Event Flow (e.g., GitHub Issue Created)
Ext->>App: POST /api/integration/{service}/events
App->>IntRouter: Route to service handler
Note over IntRouter: Verify signature (HMAC)
IntRouter->>Manager: Parse event payload
Note over Manager: Extract context (repo, issue, user)
Note over Manager: Map external user → OpenHands user
Manager->>Conv: Create conversation (with issue context)
Conv->>Sandbox: Provision sandbox
Sandbox-->>Conv: Ready
Manager->>Sandbox: Start agent with task
Note over Ext,Sandbox: Agent Works on Task...
Sandbox-->>Manager: Task complete
Manager->>Ext: POST result<br/>(PR, comment, etc.)
Note over Ext,Sandbox: Callback Flow (Agent → External Service)
Sandbox->>App: Webhook callback<br/>/api/v1/webhooks
App->>Manager: Process callback
Manager->>Ext: Update external service
```
### Supported Integrations
| Integration | Trigger Events | Agent Actions |
|-------------|----------------|---------------|
| **GitHub** | Issue created, PR opened, @mention | Create PR, comment, push commits |
| **GitLab** | Issue created, MR opened | Create MR, comment, push commits |
| **Slack** | @mention in channel | Reply in thread, create tasks |
| **Jira** | Issue created/updated | Update ticket, add comments |
| **Linear** | Issue created | Update status, add comments |
### Integration Components
| Component | Purpose | Location |
|-----------|---------|----------|
| **Integration Routes** | Webhook endpoints per service | `enterprise/server/routes/integration/` |
| **Integration Managers** | Business logic per service | `enterprise/integrations/{service}/` |
| **Token Manager** | Store/retrieve OAuth tokens | `enterprise/server/auth/token_manager.py` |
| **Callback Processor** | Handle agent → service updates | `enterprise/integrations/{service}/*_callback_processor.py` |
### Integration Authentication
```
External Service (e.g., GitHub)
┌─────────────────────────────────┐
│ GitHub App Installation │
│ - Webhook secret for signature │
│ - App private key for API calls │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ User Account Linking │
│ - Keycloak user ID │
│ - GitHub user ID │
│ - Stored OAuth tokens │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Agent Execution │
│ - Uses linked tokens for API │
│ - Can push, create PRs, comment │
└─────────────────────────────────┘
```

View File

@@ -200,7 +200,7 @@ class MetricsCollector(ABC):
"""Base class for metrics collectors."""
@abstractmethod
def collect(self) -> List[MetricResult]:
async def collect(self) -> List[MetricResult]:
"""Collect metrics and return results."""
pass
@@ -264,12 +264,13 @@ class SystemMetricsCollector(MetricsCollector):
def collector_name(self) -> str:
return "system_metrics"
def collect(self) -> List[MetricResult]:
async def collect(self) -> List[MetricResult]:
results = []
# Collect user count
with session_maker() as session:
user_count = session.query(UserSettings).count()
async with a_session_maker() as session:
user_count_result = await session.execute(select(func.count()).select_from(UserSettings))
user_count = user_count_result.scalar()
results.append(MetricResult(
key="total_users",
value=user_count
@@ -277,9 +278,11 @@ class SystemMetricsCollector(MetricsCollector):
# Collect conversation count (last 30 days)
thirty_days_ago = datetime.now(timezone.utc) - timedelta(days=30)
conversation_count = session.query(StoredConversationMetadata)\
.filter(StoredConversationMetadata.created_at >= thirty_days_ago)\
.count()
conversation_count_result = await session.execute(
select(func.count()).select_from(StoredConversationMetadata)
.where(StoredConversationMetadata.created_at >= thirty_days_ago)
)
conversation_count = conversation_count_result.scalar()
results.append(MetricResult(
key="conversations_30d",
@@ -303,7 +306,7 @@ class TelemetryCollectionProcessor(MaintenanceTaskProcessor):
"""Collect metrics from all registered collectors."""
# Check if collection is needed
if not self._should_collect():
if not await self._should_collect():
return {"status": "skipped", "reason": "too_recent"}
# Collect metrics from all registered collectors
@@ -313,7 +316,7 @@ class TelemetryCollectionProcessor(MaintenanceTaskProcessor):
for collector in collector_registry.get_all_collectors():
try:
if collector.should_collect():
results = collector.collect()
results = await collector.collect()
for result in results:
all_metrics[result.key] = result.value
collector_results[collector.collector_name] = len(results)
@@ -322,13 +325,13 @@ class TelemetryCollectionProcessor(MaintenanceTaskProcessor):
collector_results[collector.collector_name] = f"error: {e}"
# Store metrics in database
with session_maker() as session:
async with a_session_maker() as session:
telemetry_record = TelemetryMetrics(
metrics_data=all_metrics,
collected_at=datetime.now(timezone.utc)
)
session.add(telemetry_record)
session.commit()
await session.commit()
# Note: No need to track last_collection_at separately
# Can be derived from MAX(collected_at) in telemetry_metrics
@@ -339,11 +342,12 @@ class TelemetryCollectionProcessor(MaintenanceTaskProcessor):
"collectors_run": collector_results
}
def _should_collect(self) -> bool:
async def _should_collect(self) -> bool:
"""Check if collection is needed based on interval."""
with session_maker() as session:
async with a_session_maker() as session:
# Get last collection time from metrics table
last_collected = session.query(func.max(TelemetryMetrics.collected_at)).scalar()
result = await session.execute(select(func.max(TelemetryMetrics.collected_at)))
last_collected = result.scalar()
if not last_collected:
return True
@@ -366,17 +370,19 @@ class TelemetryUploadProcessor(MaintenanceTaskProcessor):
"""Upload pending metrics to Replicated."""
# Get pending metrics
with session_maker() as session:
pending_metrics = session.query(TelemetryMetrics)\
.filter(TelemetryMetrics.uploaded_at.is_(None))\
.order_by(TelemetryMetrics.collected_at)\
.all()
async with a_session_maker() as session:
result = await session.execute(
select(TelemetryMetrics)
.where(TelemetryMetrics.uploaded_at.is_(None))
.order_by(TelemetryMetrics.collected_at)
)
pending_metrics = result.scalars().all()
if not pending_metrics:
return {"status": "no_pending_metrics"}
# Get admin email - skip if not available
admin_email = self._get_admin_email()
admin_email = await self._get_admin_email()
if not admin_email:
logger.info("Skipping telemetry upload - no admin email available")
return {
@@ -413,13 +419,15 @@ class TelemetryUploadProcessor(MaintenanceTaskProcessor):
await instance.set_status(InstanceStatus.RUNNING)
# Mark as uploaded
with session_maker() as session:
record = session.query(TelemetryMetrics)\
.filter(TelemetryMetrics.id == metric_record.id)\
.first()
async with a_session_maker() as session:
result = await session.execute(
select(TelemetryMetrics)
.where(TelemetryMetrics.id == metric_record.id)
)
record = result.scalar_one_or_none()
if record:
record.uploaded_at = datetime.now(timezone.utc)
session.commit()
await session.commit()
uploaded_count += 1
@@ -427,14 +435,16 @@ class TelemetryUploadProcessor(MaintenanceTaskProcessor):
logger.error(f"Failed to upload metrics {metric_record.id}: {e}")
# Update error info
with session_maker() as session:
record = session.query(TelemetryMetrics)\
.filter(TelemetryMetrics.id == metric_record.id)\
.first()
async with a_session_maker() as session:
result = await session.execute(
select(TelemetryMetrics)
.where(TelemetryMetrics.id == metric_record.id)
)
record = result.scalar_one_or_none()
if record:
record.upload_attempts += 1
record.last_upload_error = str(e)
session.commit()
await session.commit()
failed_count += 1
@@ -448,7 +458,7 @@ class TelemetryUploadProcessor(MaintenanceTaskProcessor):
"total_processed": len(pending_metrics)
}
def _get_admin_email(self) -> str | None:
async def _get_admin_email(self) -> str | None:
"""Get administrator email for customer identification."""
# 1. Check environment variable first
env_admin_email = os.getenv('OPENHANDS_ADMIN_EMAIL')
@@ -457,12 +467,15 @@ class TelemetryUploadProcessor(MaintenanceTaskProcessor):
return env_admin_email
# 2. Use first active user's email (earliest accepted_tos)
with session_maker() as session:
first_user = session.query(UserSettings)\
.filter(UserSettings.email.isnot(None))\
.filter(UserSettings.accepted_tos.isnot(None))\
.order_by(UserSettings.accepted_tos.asc())\
.first()
async with a_session_maker() as session:
result = await session.execute(
select(UserSettings)
.where(UserSettings.email.isnot(None))
.where(UserSettings.accepted_tos.isnot(None))
.order_by(UserSettings.accepted_tos.asc())
.limit(1)
)
first_user = result.scalar_one_or_none()
if first_user and first_user.email:
logger.info(f"Using first active user email: {first_user.email}")
@@ -474,15 +487,16 @@ class TelemetryUploadProcessor(MaintenanceTaskProcessor):
async def _update_telemetry_identity(self, customer_id: str, instance_id: str) -> None:
"""Update or create telemetry identity record."""
with session_maker() as session:
identity = session.query(TelemetryIdentity).first()
async with a_session_maker() as session:
result = await session.execute(select(TelemetryIdentity).limit(1))
identity = result.scalar_one_or_none()
if not identity:
identity = TelemetryIdentity()
session.add(identity)
identity.customer_id = customer_id
identity.instance_id = instance_id
session.commit()
await session.commit()
```
### 4.4 License Warning System
@@ -503,11 +517,13 @@ async def get_license_status():
if not _is_openhands_enterprise():
return {"warn": False, "message": ""}
with session_maker() as session:
async with a_session_maker() as session:
# Get last successful upload time from metrics table
last_upload = session.query(func.max(TelemetryMetrics.uploaded_at))\
.filter(TelemetryMetrics.uploaded_at.isnot(None))\
.scalar()
result = await session.execute(
select(func.max(TelemetryMetrics.uploaded_at))
.where(TelemetryMetrics.uploaded_at.isnot(None))
)
last_upload = result.scalar()
if not last_upload:
# No successful uploads yet - show warning after 4 days
@@ -521,10 +537,13 @@ async def get_license_status():
if days_since_upload > 4:
# Find oldest unsent batch
oldest_unsent = session.query(TelemetryMetrics)\
.filter(TelemetryMetrics.uploaded_at.is_(None))\
.order_by(TelemetryMetrics.collected_at)\
.first()
result = await session.execute(
select(TelemetryMetrics)
.where(TelemetryMetrics.uploaded_at.is_(None))
.order_by(TelemetryMetrics.collected_at)
.limit(1)
)
oldest_unsent = result.scalar_one_or_none()
if oldest_unsent:
# Calculate expiration date (oldest unsent + 34 days)
@@ -630,19 +649,23 @@ spec:
- python
- -c
- |
import asyncio
from enterprise.storage.maintenance_task import MaintenanceTask, MaintenanceTaskStatus
from enterprise.storage.database import session_maker
from enterprise.storage.database import a_session_maker
from enterprise.server.telemetry.collection_processor import TelemetryCollectionProcessor
# Create collection task
processor = TelemetryCollectionProcessor()
task = MaintenanceTask()
task.set_processor(processor)
task.status = MaintenanceTaskStatus.PENDING
async def main():
# Create collection task
processor = TelemetryCollectionProcessor()
task = MaintenanceTask()
task.set_processor(processor)
task.status = MaintenanceTaskStatus.PENDING
with session_maker() as session:
session.add(task)
session.commit()
async with a_session_maker() as session:
session.add(task)
await session.commit()
asyncio.run(main())
restartPolicy: OnFailure
```
@@ -680,23 +703,27 @@ spec:
- python
- -c
- |
import asyncio
from enterprise.storage.maintenance_task import MaintenanceTask, MaintenanceTaskStatus
from enterprise.storage.database import session_maker
from enterprise.storage.database import a_session_maker
from enterprise.server.telemetry.upload_processor import TelemetryUploadProcessor
import os
# Create upload task
processor = TelemetryUploadProcessor(
replicated_publishable_key=os.getenv('REPLICATED_PUBLISHABLE_KEY'),
replicated_app_slug=os.getenv('REPLICATED_APP_SLUG', 'openhands-enterprise')
)
task = MaintenanceTask()
task.set_processor(processor)
task.status = MaintenanceTaskStatus.PENDING
async def main():
# Create upload task
processor = TelemetryUploadProcessor(
replicated_publishable_key=os.getenv('REPLICATED_PUBLISHABLE_KEY'),
replicated_app_slug=os.getenv('REPLICATED_APP_SLUG', 'openhands-enterprise')
)
task = MaintenanceTask()
task.set_processor(processor)
task.status = MaintenanceTaskStatus.PENDING
with session_maker() as session:
session.add(task)
session.commit()
async with a_session_maker() as session:
session.add(task)
await session.commit()
asyncio.run(main())
restartPolicy: OnFailure
```

View File

@@ -0,0 +1,131 @@
# Plugin Launch Flow
This document describes how plugins are launched in OpenHands Saas / Enterprise, from the plugin directory through to agent execution.
## Architecture Overview
```
Plugin Directory ──▶ Frontend /launch ──▶ App Server ──▶ Agent Server ──▶ SDK
(external) (modal) (API) (in sandbox) (plugin loading)
```
| Component | Responsibility |
|-----------|---------------|
| **Plugin Directory** | Index plugins, present to user, construct launch URLs |
| **Frontend** | Display confirmation modal, collect parameters, call API |
| **App Server** | Validate request, pass plugin specs to agent server |
| **Agent Server** | Run inside sandbox, delegate plugin loading to SDK |
| **SDK** | Fetch plugins, load contents, merge skills/hooks/MCP into agent |
## User Experience
### Plugin Directory
The plugin directory presents users with a catalog of available plugins. For each plugin, users see:
- Plugin name and description (from `plugin.json`)
- Author and version information
- A "Launch" button
When a user clicks "Launch", the plugin directory:
1. Reads the plugin's `entry_command` to know which slash command to invoke
2. Determines what parameters the plugin accepts (if any)
3. Redirects to OpenHands with this information encoded in the URL
### Parameter Collection
If a plugin requires user input (API keys, configuration values, etc.), the frontend displays a form modal before starting the conversation. Parameters are passed in the launch URL and rendered as form fields based on their type:
- **String values** → Text input
- **Number values** → Number input
- **Boolean values** → Checkbox
Only primitive types are supported. Complex types (arrays, objects) are not currently supported for parameter input.
The user fills in required values, then clicks "Start Conversation" to proceed.
## Launch Flow
1. **Plugin Directory** (external) constructs a launch URL to the OpenHands app server when user clicks "Launch":
```
/launch?plugins=BASE64_JSON&message=/city-weather:now%20Tokyo
```
The `plugins` parameter includes any parameter definitions with default values:
```json
[{
"source": "github:owner/repo",
"repo_path": "plugins/my-plugin",
"parameters": {"api_key": "", "timeout": 30, "debug": false}
}]
```
2. **OpenHands Frontend** (`/launch` route, [PR #12699](https://github.com/OpenHands/OpenHands/pull/12699)) displays modal with parameter form, collects user input
3. **OpenHands App Server** ([PR #12338](https://github.com/OpenHands/OpenHands/pull/12338)) receives the API call:
```
POST /api/v1/app-conversations
{
"plugins": [{"source": "github:owner/repo", "repo_path": "plugins/city-weather"}],
"initial_message": {"content": [{"type": "text", "text": "/city-weather:now Tokyo"}]}
}
```
Call stack:
- `AppConversationRouter` receives request with `PluginSpec` list
- `LiveStatusAppConversationService._finalize_conversation_request()` converts `PluginSpec` → `PluginSource`
- Creates `StartConversationRequest(plugins=sdk_plugins, ...)` and sends to agent server
4. **Agent Server** (inside sandbox, [SDK PR #1651](https://github.com/OpenHands/software-agent-sdk/pull/1651)) stores specs, defers loading:
Call stack:
- `ConversationService.start_conversation()` receives `StartConversationRequest`
- Creates `StoredConversation` with plugin specs
- Creates `LocalConversation(plugins=request.plugins, ...)`
- Plugin loading deferred until first `run()` or `send_message()`
5. **SDK** fetches and loads plugins on first use:
Call stack:
- `LocalConversation._ensure_plugins_loaded()` triggered by first message
- For each plugin spec:
- `Plugin.fetch(source, ref, repo_path)` → clones/caches git repo
- `Plugin.load(path)` → parses `plugin.json`, loads commands/skills/hooks
- `plugin.add_skills_to(context)` → merges skills into agent
- `plugin.add_mcp_config_to(config)` → merges MCP servers
6. **Agent** receives message, `/city-weather:now` triggers the skill
## Key Design Decisions
### Plugin Loading in Sandbox
Plugins load **inside the sandbox** because:
- Plugin hooks and scripts need isolated execution
- MCP servers run inside the sandbox
- Skills may reference sandbox filesystem
### Entry Command Handling
The `entry_command` field in `plugin.json` allows plugin authors to declare a default command:
```json
{
"name": "city-weather",
"entry_command": "now"
}
```
This flows through the system:
1. Plugin author declares `entry_command` in plugin.json
2. Plugin directory reads it when indexing
3. Plugin directory includes `/city-weather:now` in the launch URL's `message` parameter
4. Message passes through to agent as `initial_message`
The SDK exposes this field but does not auto-invoke it—callers control the initial message.
## Related
- [OpenHands PR #12338](https://github.com/OpenHands/OpenHands/pull/12338) - App server plugin support
- [OpenHands PR #12699](https://github.com/OpenHands/OpenHands/pull/12699) - Frontend `/launch` route
- [SDK PR #1651](https://github.com/OpenHands/software-agent-sdk/pull/1651) - Agent server plugin loading
- [SDK PR #1647](https://github.com/OpenHands/software-agent-sdk/pull/1647) - Plugin.fetch() for remote plugin fetching

View File

@@ -1,205 +0,0 @@
#!/usr/bin/env python
"""
Downgrade script for migrated users.
This script identifies users who have been migrated (already_migrated=True)
and reverts them back to the pre-migration state.
Usage:
# Dry run - just list the users that would be downgraded
python downgrade_migrated_users.py --dry-run
# Downgrade a specific user by their keycloak_user_id
python downgrade_migrated_users.py --user-id <user_id>
# Downgrade all migrated users (with confirmation)
python downgrade_migrated_users.py --all
# Downgrade all migrated users without confirmation (dangerous!)
python downgrade_migrated_users.py --all --no-confirm
"""
import argparse
import asyncio
import sys
# Add the enterprise directory to the path
sys.path.insert(0, '/workspace/project/OpenHands/enterprise')
from server.logger import logger
from sqlalchemy import select, text
from storage.database import session_maker
from storage.user_settings import UserSettings
from storage.user_store import UserStore
def get_migrated_users() -> list[str]:
"""Get list of keycloak_user_ids for users who have been migrated.
This includes:
1. Users with already_migrated=True in user_settings (migrated users)
2. Users in the 'user' table who don't have a user_settings entry (new sign-ups)
"""
with session_maker() as session:
# Get users from user_settings with already_migrated=True
migrated_result = session.execute(
select(UserSettings.keycloak_user_id).where(
UserSettings.already_migrated.is_(True)
)
)
migrated_users = {row[0] for row in migrated_result.fetchall() if row[0]}
# Get users from the 'user' table (new sign-ups won't have user_settings)
# These are users who signed up after the migration was deployed
new_signup_result = session.execute(
text("""
SELECT CAST(u.id AS VARCHAR)
FROM "user" u
WHERE NOT EXISTS (
SELECT 1 FROM user_settings us
WHERE us.keycloak_user_id = CAST(u.id AS VARCHAR)
)
""")
)
new_signups = {row[0] for row in new_signup_result.fetchall() if row[0]}
# Combine both sets
all_users = migrated_users | new_signups
return list(all_users)
async def downgrade_user(user_id: str) -> bool:
"""Downgrade a single user.
Args:
user_id: The keycloak_user_id to downgrade
Returns:
True if successful, False otherwise
"""
try:
result = await UserStore.downgrade_user(user_id)
if result:
print(f'✓ Successfully downgraded user: {user_id}')
return True
else:
print(f'✗ Failed to downgrade user: {user_id}')
return False
except Exception as e:
print(f'✗ Error downgrading user {user_id}: {e}')
logger.exception(
'downgrade_script:error',
extra={'user_id': user_id, 'error': str(e)},
)
return False
async def main():
parser = argparse.ArgumentParser(
description='Downgrade migrated users back to pre-migration state'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Just list users that would be downgraded, without making changes',
)
parser.add_argument(
'--user-id',
type=str,
help='Downgrade a specific user by keycloak_user_id',
)
parser.add_argument(
'--all',
action='store_true',
help='Downgrade all migrated users',
)
parser.add_argument(
'--no-confirm',
action='store_true',
help='Skip confirmation prompt (use with caution!)',
)
args = parser.parse_args()
# Get list of migrated users
migrated_users = get_migrated_users()
print(f'\nFound {len(migrated_users)} migrated user(s).')
if args.dry_run:
print('\n--- DRY RUN MODE ---')
print('The following users would be downgraded:')
for user_id in migrated_users:
print(f' - {user_id}')
print('\nNo changes were made.')
return
if args.user_id:
# Downgrade a specific user
if args.user_id not in migrated_users:
print(f'\nUser {args.user_id} is not in the migrated users list.')
print('Either the user was not migrated, or the user_id is incorrect.')
return
print(f'\nDowngrading user: {args.user_id}')
if not args.no_confirm:
confirm = input('Are you sure? (yes/no): ')
if confirm.lower() != 'yes':
print('Cancelled.')
return
success = await downgrade_user(args.user_id)
if success:
print('\nDowngrade completed successfully.')
else:
print('\nDowngrade failed. Check logs for details.')
sys.exit(1)
elif args.all:
# Downgrade all migrated users
if not migrated_users:
print('\nNo migrated users to downgrade.')
return
print(f'\n⚠️ About to downgrade {len(migrated_users)} user(s).')
if not args.no_confirm:
print('\nThis will:')
print(' - Revert LiteLLM team/user budget settings')
print(' - Delete organization entries')
print(' - Delete user entries in the new schema')
print(' - Reset the already_migrated flag')
print('\nUsers to downgrade:')
for user_id in migrated_users[:10]: # Show first 10
print(f' - {user_id}')
if len(migrated_users) > 10:
print(f' ... and {len(migrated_users) - 10} more')
confirm = input('\nType "yes" to proceed: ')
if confirm.lower() != 'yes':
print('Cancelled.')
return
print('\nStarting downgrade...\n')
success_count = 0
fail_count = 0
for user_id in migrated_users:
success = await downgrade_user(user_id)
if success:
success_count += 1
else:
fail_count += 1
print('\n--- Summary ---')
print(f'Successful: {success_count}')
print(f'Failed: {fail_count}')
if fail_count > 0:
sys.exit(1)
else:
parser.print_help()
print('\nPlease specify --dry-run, --user-id, or --all')
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -109,6 +109,9 @@ lines.append(
lines.append(
'OPENHANDS_BITBUCKET_SERVICE_CLS=integrations.bitbucket.bitbucket_service.SaaSBitBucketService'
)
lines.append(
'OPENHANDS_BITBUCKET_DATA_CENTER_SERVICE_CLS=integrations.bitbucket_data_center.bitbucket_dc_service.SaaSBitbucketDCService'
)
lines.append(
'OPENHANDS_CONVERSATION_VALIDATOR_CLS=storage.saas_conversation_validator.SaasConversationValidator'
)

View File

@@ -1,47 +0,0 @@
import os
import posthog
from openhands.core.logger import openhands_logger as logger
# Initialize PostHog
posthog.api_key = os.environ.get('POSTHOG_CLIENT_KEY', 'phc_placeholder')
posthog.host = os.environ.get('POSTHOG_HOST', 'https://us.i.posthog.com')
# Log PostHog configuration with masked API key for security
api_key = posthog.api_key
if api_key and len(api_key) > 8:
masked_key = f'{api_key[:4]}...{api_key[-4:]}'
else:
masked_key = 'not_set_or_too_short'
logger.info('posthog_configuration', extra={'posthog_api_key_masked': masked_key})
# Global toggle for the experiment manager
ENABLE_EXPERIMENT_MANAGER = (
os.environ.get('ENABLE_EXPERIMENT_MANAGER', 'false').lower() == 'true'
)
# Get the current experiment type from environment variable
# If None, no experiment is running
EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT = os.environ.get(
'EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT', ''
)
# System prompt experiment toggle
EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT = os.environ.get(
'EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT', ''
)
EXPERIMENT_CLAUDE4_VS_GPT5 = os.environ.get('EXPERIMENT_CLAUDE4_VS_GPT5', '')
EXPERIMENT_CONDENSER_MAX_STEP = os.environ.get('EXPERIMENT_CONDENSER_MAX_STEP', '')
logger.info(
'experiment_manager:run_conversation_variant_test:experiment_config',
extra={
'enable_experiment_manager': ENABLE_EXPERIMENT_MANAGER,
'experiment_litellm_default_model_experiment': EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT,
'experiment_system_prompt_experiment': EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
'experiment_claude4_vs_gpt5_experiment': EXPERIMENT_CLAUDE4_VS_GPT5,
'experiment_condenser_max_step': EXPERIMENT_CONDENSER_MAX_STEP,
},
)

View File

@@ -1,97 +0,0 @@
from uuid import UUID
from experiments.constants import (
ENABLE_EXPERIMENT_MANAGER,
EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
)
from experiments.experiment_versions import (
handle_system_prompt_experiment,
)
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.experiments.experiment_manager import ExperimentManager
from openhands.sdk import Agent
from openhands.server.session.conversation_init_data import ConversationInitData
class SaaSExperimentManager(ExperimentManager):
@staticmethod
def run_agent_variant_tests__v1(
user_id: str | None, conversation_id: UUID, agent: Agent
) -> Agent:
if not ENABLE_EXPERIMENT_MANAGER:
logger.info(
'experiment_manager:run_conversation_variant_test:skipped',
extra={'reason': 'experiment_manager_disabled'},
)
return agent
if EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT:
agent = agent.model_copy(
update={'system_prompt_filename': 'system_prompt_long_horizon.j2'}
)
return agent
@staticmethod
def run_conversation_variant_test(
user_id, conversation_id, conversation_settings
) -> ConversationInitData:
"""
Run conversation variant test and potentially modify the conversation settings
based on the PostHog feature flags.
Args:
user_id: The user ID
conversation_id: The conversation ID
conversation_settings: The conversation settings that may include convo_id and llm_model
Returns:
The modified conversation settings
"""
logger.debug(
'experiment_manager:run_conversation_variant_test:started',
extra={'user_id': user_id, 'conversation_id': conversation_id},
)
return conversation_settings
@staticmethod
def run_config_variant_test(
user_id: str | None, conversation_id: str, config: OpenHandsConfig
) -> OpenHandsConfig:
"""
Run agent config variant test and potentially modify the OpenHands config
based on the current experiment type and PostHog feature flags.
Args:
user_id: The user ID
conversation_id: The conversation ID
config: The OpenHands configuration
Returns:
The modified OpenHands configuration
"""
logger.info(
'experiment_manager:run_config_variant_test:started',
extra={'user_id': user_id},
)
# Skip all experiment processing if the experiment manager is disabled
if not ENABLE_EXPERIMENT_MANAGER:
logger.info(
'experiment_manager:run_config_variant_test:skipped',
extra={'reason': 'experiment_manager_disabled'},
)
return config
# Pass the entire OpenHands config to the system prompt experiment
# Let the experiment handler directly modify the config as needed
modified_config = handle_system_prompt_experiment(
user_id, conversation_id, config
)
# Condenser max step experiment is applied via conversation variant test,
# not config variant test. Return modified config from system prompt only.
return modified_config

View File

@@ -1,107 +0,0 @@
"""
LiteLLM model experiment handler.
This module contains the handler for the LiteLLM model experiment.
"""
import posthog
from experiments.constants import EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT
from server.constants import (
IS_FEATURE_ENV,
build_litellm_proxy_model_path,
get_default_litellm_model,
)
from openhands.core.logger import openhands_logger as logger
def handle_litellm_default_model_experiment(
user_id, conversation_id, conversation_settings
):
"""
Handle the LiteLLM model experiment.
Args:
user_id: The user ID
conversation_id: The conversation ID
conversation_settings: The conversation settings
Returns:
Modified conversation settings
"""
# No-op if the specific experiment is not enabled
if not EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT:
logger.info(
'experiment_manager:ab_testing:skipped',
extra={
'convo_id': conversation_id,
'reason': 'experiment_not_enabled',
'experiment': EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT,
},
)
return conversation_settings
# Use experiment name as the flag key
try:
enabled_variant = posthog.get_feature_flag(
EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT, conversation_id
)
except Exception as e:
logger.error(
'experiment_manager:get_feature_flag:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT,
'error': str(e),
},
)
return conversation_settings
# Log the experiment event
# If this is a feature environment, add "FEATURE_" prefix to user_id for PostHog
posthog_user_id = f'FEATURE_{user_id}' if IS_FEATURE_ENV else user_id
try:
posthog.capture(
distinct_id=posthog_user_id,
event='model_set',
properties={
'conversation_id': conversation_id,
'variant': enabled_variant,
'original_user_id': user_id,
'is_feature_env': IS_FEATURE_ENV,
},
)
except Exception as e:
logger.error(
'experiment_manager:posthog_capture:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_LITELLM_DEFAULT_MODEL_EXPERIMENT,
'error': str(e),
},
)
# Continue execution as this is not critical
logger.info(
'posthog_capture',
extra={
'event': 'model_set',
'posthog_user_id': posthog_user_id,
'is_feature_env': IS_FEATURE_ENV,
'conversation_id': conversation_id,
'variant': enabled_variant,
},
)
# Set the model based on the feature flag variant
if enabled_variant == 'claude37':
# Use the shared utility to construct the LiteLLM proxy model path
model = build_litellm_proxy_model_path('claude-3-7-sonnet-20250219')
# Update the conversation settings with the selected model
conversation_settings.llm_model = model
else:
# Update the conversation settings with the default model for the current version
conversation_settings.llm_model = get_default_litellm_model()
return conversation_settings

View File

@@ -1,181 +0,0 @@
"""
System prompt experiment handler.
This module contains the handler for the system prompt experiment that uses
the PostHog variant as the system prompt filename.
"""
import copy
import posthog
from experiments.constants import EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT
from server.constants import IS_FEATURE_ENV
from storage.experiment_assignment_store import ExperimentAssignmentStore
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
def _get_system_prompt_variant(user_id, conversation_id):
"""
Get the system prompt variant for the experiment.
Args:
user_id: The user ID
conversation_id: The conversation ID
Returns:
str or None: The PostHog variant name or None if experiment is not enabled or error occurs
"""
# No-op if the specific experiment is not enabled
if not EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT:
logger.info(
'experiment_manager_002:ab_testing:skipped',
extra={
'convo_id': conversation_id,
'reason': 'experiment_not_enabled',
'experiment': EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
},
)
return None
# Use experiment name as the flag key
try:
enabled_variant = posthog.get_feature_flag(
EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT, conversation_id
)
except Exception as e:
logger.error(
'experiment_manager:get_feature_flag:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
'error': str(e),
},
)
return None
# Store the experiment assignment in the database
try:
experiment_store = ExperimentAssignmentStore()
experiment_store.update_experiment_variant(
conversation_id=conversation_id,
experiment_name='system_prompt_experiment',
variant=enabled_variant,
)
except Exception as e:
logger.error(
'experiment_manager:store_assignment:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
'variant': enabled_variant,
'error': str(e),
},
)
# Fail the experiment if we cannot track the splits - results would not be explainable
return None
# Log the experiment event
# If this is a feature environment, add "FEATURE_" prefix to user_id for PostHog
posthog_user_id = f'FEATURE_{user_id}' if IS_FEATURE_ENV else user_id
try:
posthog.capture(
distinct_id=posthog_user_id,
event='system_prompt_set',
properties={
'conversation_id': conversation_id,
'variant': enabled_variant,
'original_user_id': user_id,
'is_feature_env': IS_FEATURE_ENV,
},
)
except Exception as e:
logger.error(
'experiment_manager:posthog_capture:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
'error': str(e),
},
)
# Continue execution as this is not critical
logger.info(
'posthog_capture',
extra={
'event': 'system_prompt_set',
'posthog_user_id': posthog_user_id,
'is_feature_env': IS_FEATURE_ENV,
'conversation_id': conversation_id,
'variant': enabled_variant,
},
)
return enabled_variant
def handle_system_prompt_experiment(
user_id, conversation_id, config: OpenHandsConfig
) -> OpenHandsConfig:
"""
Handle the system prompt experiment for OpenHands config.
Args:
user_id: The user ID
conversation_id: The conversation ID
config: The OpenHands configuration
Returns:
Modified OpenHands configuration
"""
enabled_variant = _get_system_prompt_variant(user_id, conversation_id)
# If variant is None, experiment is not enabled or there was an error
if enabled_variant is None:
return config
# Deep copy the config to avoid modifying the original
modified_config = copy.deepcopy(config)
# Set the system prompt filename based on the variant
if enabled_variant == 'control':
# Use the long-horizon system prompt for the control variant
agent_config = modified_config.get_agent_config(modified_config.default_agent)
agent_config.system_prompt_filename = 'system_prompt_long_horizon.j2'
agent_config.enable_plan_mode = True
elif enabled_variant == 'interactive':
modified_config.get_agent_config(
modified_config.default_agent
).system_prompt_filename = 'system_prompt_interactive.j2'
elif enabled_variant == 'no_tools':
modified_config.get_agent_config(
modified_config.default_agent
).system_prompt_filename = 'system_prompt.j2'
else:
logger.error(
'system_prompt_experiment:unknown_variant',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'variant': enabled_variant,
'reason': 'no explicit mapping; returning original config',
},
)
return config
# Log which prompt is being used
logger.info(
'system_prompt_experiment:prompt_selected',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'system_prompt_filename': modified_config.get_agent_config(
modified_config.default_agent
).system_prompt_filename,
'variant': enabled_variant,
},
)
return modified_config

View File

@@ -1,137 +0,0 @@
"""
LiteLLM model experiment handler.
This module contains the handler for the LiteLLM model experiment.
"""
import posthog
from experiments.constants import EXPERIMENT_CLAUDE4_VS_GPT5
from server.constants import (
IS_FEATURE_ENV,
build_litellm_proxy_model_path,
get_default_litellm_model,
)
from storage.experiment_assignment_store import ExperimentAssignmentStore
from openhands.core.logger import openhands_logger as logger
from openhands.server.session.conversation_init_data import ConversationInitData
def _get_model_variant(user_id: str | None, conversation_id: str) -> str | None:
if not EXPERIMENT_CLAUDE4_VS_GPT5:
logger.info(
'experiment_manager:ab_testing:skipped',
extra={
'convo_id': conversation_id,
'reason': 'experiment_not_enabled',
'experiment': EXPERIMENT_CLAUDE4_VS_GPT5,
},
)
return None
try:
enabled_variant = posthog.get_feature_flag(
EXPERIMENT_CLAUDE4_VS_GPT5, conversation_id
)
except Exception as e:
logger.error(
'experiment_manager:get_feature_flag:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_CLAUDE4_VS_GPT5,
'error': str(e),
},
)
return None
# Store the experiment assignment in the database
try:
experiment_store = ExperimentAssignmentStore()
experiment_store.update_experiment_variant(
conversation_id=conversation_id,
experiment_name='claude4_vs_gpt5_experiment',
variant=enabled_variant,
)
except Exception as e:
logger.error(
'experiment_manager:store_assignment:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_CLAUDE4_VS_GPT5,
'variant': enabled_variant,
'error': str(e),
},
)
# Fail the experiment if we cannot track the splits - results would not be explainable
return None
# Log the experiment event
# If this is a feature environment, add "FEATURE_" prefix to user_id for PostHog
posthog_user_id = f'FEATURE_{user_id}' if IS_FEATURE_ENV else user_id
try:
posthog.capture(
distinct_id=posthog_user_id,
event='claude4_or_gpt5_set',
properties={
'conversation_id': conversation_id,
'variant': enabled_variant,
'original_user_id': user_id,
'is_feature_env': IS_FEATURE_ENV,
},
)
except Exception as e:
logger.error(
'experiment_manager:posthog_capture:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_CLAUDE4_VS_GPT5,
'error': str(e),
},
)
# Continue execution as this is not critical
logger.info(
'posthog_capture',
extra={
'event': 'claude4_or_gpt5_set',
'posthog_user_id': posthog_user_id,
'is_feature_env': IS_FEATURE_ENV,
'conversation_id': conversation_id,
'variant': enabled_variant,
},
)
return enabled_variant
def handle_claude4_vs_gpt5_experiment(
user_id: str | None,
conversation_id: str,
conversation_settings: ConversationInitData,
) -> ConversationInitData:
"""
Handle the LiteLLM model experiment.
Args:
user_id: The user ID
conversation_id: The conversation ID
conversation_settings: The conversation settings
Returns:
Modified conversation settings
"""
enabled_variant = _get_model_variant(user_id, conversation_id)
if not enabled_variant:
return conversation_settings
# Set the model based on the feature flag variant
if enabled_variant == 'gpt5':
model = build_litellm_proxy_model_path('gpt-5-2025-08-07')
conversation_settings.llm_model = model
else:
conversation_settings.llm_model = get_default_litellm_model()
return conversation_settings

View File

@@ -1,232 +0,0 @@
"""
Condenser max step experiment handler.
This module contains the handler for the condenser max step experiment that tests
different max_size values for the condenser configuration.
"""
from uuid import UUID
import posthog
from experiments.constants import EXPERIMENT_CONDENSER_MAX_STEP
from server.constants import IS_FEATURE_ENV
from storage.experiment_assignment_store import ExperimentAssignmentStore
from openhands.core.logger import openhands_logger as logger
from openhands.sdk import Agent
from openhands.sdk.context.condenser import (
LLMSummarizingCondenser,
)
from openhands.server.session.conversation_init_data import ConversationInitData
def _get_condenser_max_step_variant(user_id, conversation_id):
"""
Get the condenser max step variant for the experiment.
Args:
user_id: The user ID
conversation_id: The conversation ID
Returns:
str or None: The PostHog variant name or None if experiment is not enabled or error occurs
"""
# No-op if the specific experiment is not enabled
if not EXPERIMENT_CONDENSER_MAX_STEP:
logger.info(
'experiment_manager_004:ab_testing:skipped',
extra={
'convo_id': conversation_id,
'reason': 'experiment_not_enabled',
'experiment': EXPERIMENT_CONDENSER_MAX_STEP,
},
)
return None
# Use experiment name as the flag key
try:
enabled_variant = posthog.get_feature_flag(
EXPERIMENT_CONDENSER_MAX_STEP, conversation_id
)
except Exception as e:
logger.error(
'experiment_manager:get_feature_flag:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_CONDENSER_MAX_STEP,
'error': str(e),
},
)
return None
# Store the experiment assignment in the database
try:
experiment_store = ExperimentAssignmentStore()
experiment_store.update_experiment_variant(
conversation_id=conversation_id,
experiment_name='condenser_max_step_experiment',
variant=enabled_variant,
)
except Exception as e:
logger.error(
'experiment_manager:store_assignment:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_CONDENSER_MAX_STEP,
'variant': enabled_variant,
'error': str(e),
},
)
# Fail the experiment if we cannot track the splits - results would not be explainable
return None
# Log the experiment event
# If this is a feature environment, add "FEATURE_" prefix to user_id for PostHog
posthog_user_id = f'FEATURE_{user_id}' if IS_FEATURE_ENV else user_id
try:
posthog.capture(
distinct_id=posthog_user_id,
event='condenser_max_step_set',
properties={
'conversation_id': conversation_id,
'variant': enabled_variant,
'original_user_id': user_id,
'is_feature_env': IS_FEATURE_ENV,
},
)
except Exception as e:
logger.error(
'experiment_manager:posthog_capture:failed',
extra={
'convo_id': conversation_id,
'experiment': EXPERIMENT_CONDENSER_MAX_STEP,
'error': str(e),
},
)
# Continue execution as this is not critical
logger.info(
'posthog_capture',
extra={
'event': 'condenser_max_step_set',
'posthog_user_id': posthog_user_id,
'is_feature_env': IS_FEATURE_ENV,
'conversation_id': conversation_id,
'variant': enabled_variant,
},
)
return enabled_variant
def handle_condenser_max_step_experiment(
user_id: str | None,
conversation_id: str,
conversation_settings: ConversationInitData,
) -> ConversationInitData:
"""
Handle the condenser max step experiment for conversation settings.
We should not modify persistent user settings. Instead, apply the experiment
variant to the conversation's in-memory settings object for this session only.
Variants:
- control -> condenser_max_size = 120
- treatment -> condenser_max_size = 80
Returns the (potentially) modified conversation_settings.
"""
enabled_variant = _get_condenser_max_step_variant(user_id, conversation_id)
if enabled_variant is None:
return conversation_settings
if enabled_variant == 'control':
condenser_max_size = 120
elif enabled_variant == 'treatment':
condenser_max_size = 80
else:
logger.error(
'condenser_max_step_experiment:unknown_variant',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'variant': enabled_variant,
'reason': 'unknown variant; returning original conversation settings',
},
)
return conversation_settings
try:
# Apply the variant to this conversation only; do not persist to DB.
# Not all OpenHands versions expose `condenser_max_size` on settings.
if hasattr(conversation_settings, 'condenser_max_size'):
conversation_settings.condenser_max_size = condenser_max_size
logger.info(
'condenser_max_step_experiment:conversation_settings_applied',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'variant': enabled_variant,
'condenser_max_size': condenser_max_size,
},
)
else:
logger.warning(
'condenser_max_step_experiment:field_missing_on_settings',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'variant': enabled_variant,
'reason': 'condenser_max_size not present on ConversationInitData',
},
)
except Exception as e:
logger.error(
'condenser_max_step_experiment:apply_failed',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'variant': enabled_variant,
'error': str(e),
},
)
return conversation_settings
return conversation_settings
def handle_condenser_max_step_experiment__v1(
user_id: str | None,
conversation_id: UUID,
agent: Agent,
) -> Agent:
enabled_variant = _get_condenser_max_step_variant(user_id, str(conversation_id))
if enabled_variant is None:
return agent
if enabled_variant == 'control':
condenser_max_size = 120
elif enabled_variant == 'treatment':
condenser_max_size = 80
else:
logger.error(
'condenser_max_step_experiment:unknown_variant',
extra={
'user_id': user_id,
'convo_id': conversation_id,
'variant': enabled_variant,
'reason': 'unknown variant; returning original conversation settings',
},
)
return agent
condenser_llm = agent.llm.model_copy(update={'usage_id': 'condenser'})
condenser = LLMSummarizingCondenser(
llm=condenser_llm, max_size=condenser_max_size, keep_first=4
)
return agent.model_copy(update={'condenser': condenser})

View File

@@ -1,25 +0,0 @@
"""
Experiment versions package.
This package contains handlers for different experiment versions.
"""
from experiments.experiment_versions._001_litellm_default_model_experiment import (
handle_litellm_default_model_experiment,
)
from experiments.experiment_versions._002_system_prompt_experiment import (
handle_system_prompt_experiment,
)
from experiments.experiment_versions._003_llm_claude4_vs_gpt5_experiment import (
handle_claude4_vs_gpt5_experiment,
)
from experiments.experiment_versions._004_condenser_max_step_experiment import (
handle_condenser_max_step_experiment,
)
__all__ = [
'handle_litellm_default_model_experiment',
'handle_system_prompt_experiment',
'handle_claude4_vs_gpt5_experiment',
'handle_condenser_max_step_experiment',
]

View File

@@ -0,0 +1,65 @@
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.bitbucket_data_center.bitbucket_dc_service import (
BitbucketDCService,
)
from openhands.integrations.service_types import ProviderType
class SaaSBitbucketDCService(BitbucketDCService):
def __init__(
self,
user_id: str | None = None,
external_auth_token: SecretStr | None = None,
external_auth_id: str | None = None,
token: SecretStr | None = None,
external_token_manager: bool = False,
base_domain: str | None = None,
):
logger.debug(
f'SaaSBitbucketDCService created with user_id {user_id}, external_auth_id {external_auth_id}, external_auth_token {'set' if external_auth_token else 'None'}, token {'set' if token else 'None'}, external_token_manager {external_token_manager}'
)
super().__init__(
user_id=user_id,
external_auth_token=external_auth_token,
external_auth_id=external_auth_id,
token=token,
external_token_manager=external_token_manager,
base_domain=base_domain,
)
self.token_manager = TokenManager(external=external_token_manager)
self.refresh = True
async def get_latest_token(self) -> SecretStr | None:
bitbucket_dc_token = None
if self.external_auth_token:
bitbucket_dc_token = SecretStr(
await self.token_manager.get_idp_token(
self.external_auth_token.get_secret_value(),
idp=ProviderType.BITBUCKET_DATA_CENTER,
)
)
logger.debug('Got Bitbucket DC token via external_auth_token')
elif self.external_auth_id:
offline_token = await self.token_manager.load_offline_token(
self.external_auth_id
)
bitbucket_dc_token = SecretStr(
await self.token_manager.get_idp_token_from_offline_token(
offline_token, ProviderType.BITBUCKET_DATA_CENTER
)
)
logger.debug('Got Bitbucket DC token via external_auth_id')
elif self.user_id:
bitbucket_dc_token = SecretStr(
await self.token_manager.get_idp_token_from_idp_user_id(
self.user_id, ProviderType.BITBUCKET_DATA_CENTER
)
)
logger.debug('Got Bitbucket DC token via user_id')
else:
logger.warning('external_auth_token and user_id not set!')
return bitbucket_dc_token

View File

@@ -116,10 +116,8 @@ class GitHubDataCollector:
return suffix
def _get_installation_access_token(self, installation_id: str) -> str:
token_data = self.github_integration.get_access_token(
installation_id # type: ignore[arg-type]
)
def _get_installation_access_token(self, installation_id: int) -> str:
token_data = self.github_integration.get_access_token(installation_id)
return token_data.token
def _check_openhands_author(self, name, login) -> bool:
@@ -134,7 +132,7 @@ class GitHubDataCollector:
)
def _get_issue_comments(
self, installation_id: str, repo_name: str, issue_number: int, conversation_id
self, installation_id: int, repo_name: str, issue_number: int, conversation_id
) -> list[dict[str, Any]]:
"""
Retrieve all comments from an issue until a comment with conversation_id is found
@@ -234,7 +232,7 @@ class GitHubDataCollector:
f'[Github]: Saved issue #{issue_number} for {github_view.full_repo_name}'
)
def _get_pr_commits(self, installation_id: str, repo_name: str, pr_number: int):
def _get_pr_commits(self, installation_id: int, repo_name: str, pr_number: int):
commits = []
installation_token = self._get_installation_access_token(installation_id)
with Github(auth=Auth.Token(installation_token)) as github_client:
@@ -431,7 +429,7 @@ class GitHubDataCollector:
- Num openhands review comments
"""
pr_number = openhands_pr.pr_number
installation_id = openhands_pr.installation_id
installation_id = int(openhands_pr.installation_id)
repo_id = openhands_pr.repo_id
# Get installation token and create Github client
@@ -569,7 +567,7 @@ class GitHubDataCollector:
openhands_helped_author = openhands_commit_count > 0
# Update the PR with OpenHands statistics
update_success = store.update_pr_openhands_stats(
update_success = await store.update_pr_openhands_stats(
repo_id=repo_id,
pr_number=pr_number,
original_updated_at=openhands_pr.updated_at,
@@ -612,7 +610,7 @@ class GitHubDataCollector:
action = payload.get('action', '')
return action == 'closed' and 'pull_request' in payload
def _track_closed_or_merged_pr(self, payload):
async def _track_closed_or_merged_pr(self, payload):
"""
Track PR closed/merged event
"""
@@ -671,17 +669,17 @@ class GitHubDataCollector:
num_general_comments=num_general_comments,
)
store.insert_pr(pr)
await store.insert_pr(pr)
logger.info(f'Tracked PR {status}: {repo_id}#{pr_number}')
def process_payload(self, message: Message):
async def process_payload(self, message: Message):
if not COLLECT_GITHUB_INTERACTIONS:
return
raw_payload = message.message.get('payload', {})
if self._is_pr_closed_or_merged(raw_payload):
self._track_closed_or_merged_pr(raw_payload)
await self._track_closed_or_merged_pr(raw_payload)
async def save_data(self, github_view: ResolverViewInterface):
if not COLLECT_GITHUB_INTERACTIONS:

View File

@@ -10,6 +10,7 @@ from integrations.github.github_view import (
GithubIssue,
GithubIssueComment,
GithubPRComment,
GithubViewType,
)
from integrations.manager import Manager
from integrations.models import (
@@ -19,9 +20,11 @@ from integrations.models import (
from integrations.types import ResolverViewInterface
from integrations.utils import (
CONVERSATION_URL,
ENABLE_SOLVABILITY_ANALYSIS,
HOST_URL,
OPENHANDS_RESOLVER_TEMPLATES_DIR,
get_session_expired_message,
get_user_not_found_message,
)
from integrations.v1_utils import get_saas_user_auth
from jinja2 import Environment, FileSystemLoader
@@ -29,7 +32,6 @@ from pydantic import SecretStr
from server.auth.auth_error import ExpiredError
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from server.auth.token_manager import TokenManager
from server.utils.conversation_callback_utils import register_callback_processor
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderToken, ProviderType
@@ -40,10 +42,9 @@ from openhands.server.types import (
SessionExpiredError,
)
from openhands.storage.data_models.secrets import Secrets
from openhands.utils.async_utils import call_sync_from_async
class GithubManager(Manager):
class GithubManager(Manager[GithubViewType]):
def __init__(
self, token_manager: TokenManager, data_collector: GitHubDataCollector
):
@@ -67,11 +68,8 @@ class GithubManager(Manager):
return f'{owner}/{repo_name}'
def _get_installation_access_token(self, installation_id: str) -> str:
# get_access_token is typed to only accept int, but it can handle str.
token_data = self.github_integration.get_access_token(
installation_id # type: ignore[arg-type]
)
def _get_installation_access_token(self, installation_id: int) -> str:
token_data = self.github_integration.get_access_token(installation_id)
return token_data.token
def _add_reaction(
@@ -126,6 +124,76 @@ class GithubManager(Manager):
return False
def _get_issue_number_from_payload(self, message: Message) -> int | None:
"""Extract issue/PR number from a GitHub webhook payload.
Supports all event types that can trigger jobs:
- Labeled issues: payload['issue']['number']
- Issue comments: payload['issue']['number']
- PR comments: payload['issue']['number'] (PRs are accessed via issue endpoint)
- Inline PR comments: payload['pull_request']['number']
Args:
message: The incoming GitHub webhook message
Returns:
The issue/PR number, or None if not found
"""
payload = message.message.get('payload', {})
# Labeled issues, issue comments, and PR comments all have 'issue' in payload
if 'issue' in payload:
return payload['issue']['number']
# Inline PR comments have 'pull_request' directly in payload
if 'pull_request' in payload:
return payload['pull_request']['number']
return None
def _send_user_not_found_message(self, message: Message, username: str):
"""Send a message to the user informing them they need to create an OpenHands account.
This method handles all supported trigger types:
- Labeled issues (action='labeled' with openhands label)
- Issue comments (comment containing @openhands)
- PR comments (comment containing @openhands on a PR)
- Inline PR review comments (comment containing @openhands)
Args:
message: The incoming GitHub webhook message
username: The GitHub username to mention in the response
"""
payload = message.message.get('payload', {})
installation_id = message.message['installation']
repo_obj = payload['repository']
full_repo_name = self._get_full_repo_name(repo_obj)
# Get installation token to post the comment
installation_token = self._get_installation_access_token(installation_id)
# Determine the issue/PR number based on the event type
issue_number = self._get_issue_number_from_payload(message)
if not issue_number:
logger.warning(
f'[GitHub] Could not determine issue/PR number to send user not found message for {username}. '
f'Payload keys: {list(payload.keys())}'
)
return
# Post the comment
try:
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(full_repo_name)
issue = repo.get_issue(number=issue_number)
issue.create_comment(get_user_not_found_message(username))
except Exception as e:
logger.error(
f'[GitHub] Failed to send user not found message to {username} '
f'on {full_repo_name}#{issue_number}: {e}'
)
async def is_job_requested(self, message: Message) -> bool:
self._confirm_incoming_source_type(message)
@@ -145,11 +213,7 @@ class GithubManager(Manager):
).get('body', ''):
return False
if GithubFactory.is_eligible_for_conversation_starter(
message
) and self._user_has_write_access_to_repo(installation_id, repo_name, username):
await GithubFactory.trigger_conversation_starter(message)
# Check event types before making expensive API calls (e.g., _user_has_write_access_to_repo)
if not (
GithubFactory.is_labeled_issue(message)
or GithubFactory.is_issue_comment(message)
@@ -159,13 +223,22 @@ class GithubManager(Manager):
return False
logger.info(f'[GitHub] Checking permissions for {username} in {repo_name}')
user_has_write_access = self._user_has_write_access_to_repo(
installation_id, repo_name, username
)
return self._user_has_write_access_to_repo(installation_id, repo_name, username)
if (
GithubFactory.is_eligible_for_conversation_starter(message)
and user_has_write_access
):
await GithubFactory.trigger_conversation_starter(message)
return user_has_write_access
async def receive_message(self, message: Message):
self._confirm_incoming_source_type(message)
try:
await call_sync_from_async(self.data_collector.process_payload, message)
await self.data_collector.process_payload(message)
except Exception:
logger.warning(
'[Github]: Error processing payload for gh interaction', exc_info=True
@@ -174,9 +247,20 @@ class GithubManager(Manager):
if await self.is_job_requested(message):
payload = message.message.get('payload', {})
user_id = payload['sender']['id']
username = payload['sender']['login']
keycloak_user_id = await self.token_manager.get_user_id_from_idp_user_id(
user_id, ProviderType.GITHUB
)
# Check if the user has an OpenHands account
if not keycloak_user_id:
logger.warning(
f'[GitHub] User {username} (id={user_id}) not found in Keycloak. '
f'User must create an OpenHands account first.'
)
self._send_user_not_found_message(message, username)
return
github_view = await GithubFactory.create_github_view_from_payload(
message, keycloak_user_id
)
@@ -188,59 +272,59 @@ class GithubManager(Manager):
github_view.installation_id
)
# Store the installation token
self.token_manager.store_org_token(
await self.token_manager.store_org_token(
github_view.installation_id, installation_token
)
# Add eyes reaction to acknowledge we've read the request
self._add_reaction(github_view, 'eyes', installation_token)
await self.start_job(github_view)
async def send_message(self, message: Message, github_view: ResolverViewInterface):
installation_token = self.token_manager.load_org_token(
async def send_message(self, message: str, github_view: GithubViewType):
"""Send a message to GitHub.
Args:
message: The message content to send (plain text string)
github_view: The GitHub view object containing issue/PR/comment info
"""
installation_token = await self.token_manager.load_org_token(
github_view.installation_id
)
if not installation_token:
logger.warning('Missing installation token')
return
outgoing_message = message.message
if isinstance(github_view, GithubInlinePRComment):
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(github_view.full_repo_name)
pr = repo.get_pull(github_view.issue_number)
pr.create_review_comment_reply(
comment_id=github_view.comment_id, body=outgoing_message
comment_id=github_view.comment_id, body=message
)
elif (
isinstance(github_view, GithubPRComment)
or isinstance(github_view, GithubIssueComment)
or isinstance(github_view, GithubIssue)
elif isinstance(
github_view, (GithubPRComment, GithubIssueComment, GithubIssue)
):
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(github_view.full_repo_name)
issue = repo.get_issue(number=github_view.issue_number)
issue.create_comment(outgoing_message)
issue.create_comment(message)
else:
logger.warning('Unsupported location')
# Catch any new types added to GithubViewType that aren't handled above
logger.warning( # type: ignore[unreachable]
f'Unsupported github_view type: {type(github_view).__name__}'
)
return
async def start_job(self, github_view: ResolverViewInterface):
"""Kick off a job with openhands agent.
async def start_job(self, github_view: GithubViewType) -> None:
"""Kick off a job with openhands agent using V1 app conversation system.
1. Get user credential
2. Initialize new conversation with repo
3. Save interaction data
"""
# Importing here prevents circular import
from server.conversation_callback_processor.github_callback_processor import (
GithubCallbackProcessor,
)
try:
msg_info = None
msg_info: str = ''
try:
user_info = github_view.user_info
@@ -281,19 +365,19 @@ class GithubManager(Manager):
# 3. Once the conversation is started, its base cost will include the report's spend as well which allows us to control max budget per resolver task
convo_metadata = await github_view.initialize_new_conversation()
solvability_summary = None
try:
if user_token:
if not ENABLE_SOLVABILITY_ANALYSIS:
logger.info(
'[Github]: Solvability report feature is disabled, skipping'
)
else:
try:
solvability_summary = await summarize_issue_solvability(
github_view, user_token
)
else:
except Exception as e:
logger.warning(
'[Github]: No user token available for solvability analysis'
f'[Github]: Error summarizing issue solvability: {str(e)}'
)
except Exception as e:
logger.warning(
f'[Github]: Error summarizing issue solvability: {str(e)}'
)
saas_user_auth = await get_saas_user_auth(
github_view.user_info.keycloak_user_id, self.token_manager
@@ -312,19 +396,7 @@ class GithubManager(Manager):
f'[GitHub] Created conversation {conversation_id} for user {user_info.username}'
)
if not github_view.v1_enabled:
# Create a GithubCallbackProcessor
processor = GithubCallbackProcessor(
github_view=github_view,
send_summary_instruction=True,
)
# Register the callback processor
register_callback_processor(conversation_id, processor)
logger.info(
f'[Github] Registered callback processor for conversation {conversation_id}'
)
# V1 callback processors are registered by the view during conversation creation
# Send message with conversation link
conversation_link = CONVERSATION_URL.format(conversation_id)
@@ -356,15 +428,13 @@ class GithubManager(Manager):
msg_info = get_session_expired_message(user_info.username)
msg = self.create_outgoing_message(msg_info)
await self.send_message(msg, github_view)
await self.send_message(msg_info, github_view)
except Exception:
logger.exception('[Github]: Error starting job')
msg = self.create_outgoing_message(
msg='Uh oh! There was an unexpected error starting the job :('
await self.send_message(
'Uh oh! There was an unexpected error starting the job :(', github_view
)
await self.send_message(msg, github_view)
try:
await self.data_collector.save_data(github_view)

View File

@@ -122,13 +122,37 @@ class SaaSGitHubService(GitHubService):
raise Exception(f'No node_id found for repository {repo_id}')
return node_id
async def _get_external_auth_id(self) -> str | None:
"""Get or fetch external_auth_id from Keycloak token if not already set."""
if self.external_auth_id:
return self.external_auth_id
if self.external_auth_token:
try:
user_info = await self.token_manager.get_user_info(
self.external_auth_token.get_secret_value()
)
self.external_auth_id = user_info.sub
logger.info(
f'Determined external_auth_id from Keycloak token: {self.external_auth_id}'
)
return self.external_auth_id
except Exception as e:
logger.warning(
f'Could not determine external_auth_id from token: {e}',
exc_info=True,
)
return None
async def get_paginated_repos(self, page, per_page, sort, installation_id):
repositories = await super().get_paginated_repos(
page, per_page, sort, installation_id
)
asyncio.create_task(
store_repositories_in_db(repositories, self.external_auth_id)
)
external_auth_id = await self._get_external_auth_id()
if external_auth_id:
asyncio.create_task(
store_repositories_in_db(repositories, external_auth_id)
)
return repositories
async def get_all_repositories(
@@ -136,8 +160,10 @@ class SaaSGitHubService(GitHubService):
) -> list[Repository]:
repositories = await super().get_all_repositories(sort, app_mode)
# Schedule the background task without awaiting it
asyncio.create_task(
store_repositories_in_db(repositories, self.external_auth_id)
)
external_auth_id = await self._get_external_auth_id()
if external_auth_id:
asyncio.create_task(
store_repositories_in_db(repositories, external_auth_id)
)
# Return repositories immediately
return repositories

View File

@@ -14,7 +14,6 @@ from integrations.solvability.models.summary import SolvabilitySummary
from integrations.utils import ENABLE_SOLVABILITY_ANALYSIS
from pydantic import ValidationError
from server.config import get_config
from storage.database import session_maker
from storage.saas_settings_store import SaasSettingsStore
from openhands.core.config import LLMConfig
@@ -90,7 +89,6 @@ async def summarize_issue_solvability(
# Grab the user's information so we can load their LLM configuration
store = SaasSettingsStore(
user_id=github_view.user_info.keycloak_user_id,
session_maker=session_maker,
config=get_config(),
)
@@ -108,11 +106,18 @@ async def summarize_issue_solvability(
f'Solvability analysis disabled for user {github_view.user_info.user_id}'
)
agent_settings = user_settings.agent_settings
llm_settings = agent_settings.llm
if llm_settings.api_key is None:
raise ValueError(
f'[Solvability] No LLM API key found for user {github_view.user_info.user_id}'
)
try:
llm_config = LLMConfig(
model=user_settings.llm_model,
api_key=user_settings.llm_api_key.get_secret_value(),
base_url=user_settings.llm_base_url,
model=llm_settings.model,
api_key=llm_settings.api_key.get_secret_value(),
base_url=llm_settings.base_url,
)
except ValidationError as e:
raise ValueError(

View File

@@ -3,8 +3,9 @@ from typing import Any
from uuid import UUID
import httpx
from github import Auth, Github, GithubIntegration
from integrations.utils import CONVERSATION_URL, get_summary_instruction
from github import Auth, Github, GithubException, GithubIntegration
from integrations.utils import get_summary_instruction
from integrations.v1_utils import handle_callback_error
from pydantic import Field
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
@@ -42,16 +43,20 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
event: Event,
) -> EventCallbackResult | None:
"""Process events for GitHub V1 integration."""
# Only handle ConversationStateUpdateEvent
# Only handle ConversationStateUpdateEvent for execution_status
if not isinstance(event, ConversationStateUpdateEvent):
return None
# Only act when execution has finished
if not (event.key == 'execution_status' and event.value == 'finished'):
if event.key != 'execution_status':
return None
# Log ALL terminal states for monitoring (finished, error, stuck)
_logger.info('[GitHub V1] Callback agent state was %s', event)
# Only request summary when execution has finished successfully
if event.value != 'finished':
return None
_logger.info(
'[GitHub V1] Should request summary: %s', self.should_request_summary
)
@@ -78,25 +83,20 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
detail=summary,
)
except Exception as e:
_logger.exception('[GitHub V1] Error processing callback: %s', e)
# Only try to post error to GitHub if we have basic requirements
try:
# Check if we have installation ID and credentials before posting
if (
self.github_view_data.get('installation_id')
and GITHUB_APP_CLIENT_ID
and GITHUB_APP_PRIVATE_KEY
):
await self._post_summary_to_github(
f'OpenHands encountered an error: **{str(e)}**.\n\n'
f'[See the conversation]({CONVERSATION_URL.format(conversation_id)})'
'for more information.'
)
except Exception as post_error:
_logger.warning(
'[GitHub V1] Failed to post error message to GitHub: %s', post_error
)
# Check if we have installation ID and credentials before posting
can_post_error = bool(
self.github_view_data.get('installation_id')
and GITHUB_APP_CLIENT_ID
and GITHUB_APP_PRIVATE_KEY
)
await handle_callback_error(
error=e,
conversation_id=conversation_id,
service_name='GitHub',
service_logger=_logger,
can_post_error=can_post_error,
post_error_func=self._post_summary_to_github,
)
return EventCallbackResult(
status=EventCallbackResultStatus.ERROR,
@@ -137,19 +137,30 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
full_repo_name = self.github_view_data['full_repo_name']
issue_number = self.github_view_data['issue_number']
if self.inline_pr_comment:
try:
if self.inline_pr_comment:
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(full_repo_name)
pr = repo.get_pull(issue_number)
pr.create_review_comment_reply(
comment_id=self.github_view_data.get('comment_id', ''),
body=summary,
)
return
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(full_repo_name)
pr = repo.get_pull(issue_number)
pr.create_review_comment_reply(
comment_id=self.github_view_data.get('comment_id', ''), body=summary
issue = repo.get_issue(number=issue_number)
issue.create_comment(summary)
except GithubException as e:
if e.status == 410:
_logger.info(
'[GitHub V1] Issue/PR %s#%s was deleted, skipping summary post',
full_repo_name,
issue_number,
)
return
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(full_repo_name)
issue = repo.get_issue(number=issue_number)
issue.create_comment(summary)
else:
raise
# -------------------------------------------------------------------------
# Agent / sandbox helpers
@@ -167,8 +178,8 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
send_message_request = AskAgentRequest(question=message_content)
url = (
f'{agent_server_url.rstrip("/")}'
f'/api/conversations/{conversation_id}/ask_agent'
f"{agent_server_url.rstrip('/')}"
f"/api/conversations/{conversation_id}/ask_agent"
)
headers = {'X-Session-API-Key': session_api_key}
payload = send_message_request.model_dump()
@@ -230,8 +241,7 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
# -------------------------------------------------------------------------
async def _request_summary(self, conversation_id: UUID) -> str:
"""
Ask the agent to produce a summary of its work and return the agent response.
"""Ask the agent to produce a summary of its work and return the agent response.
NOTE: This method now returns a string (the agent server's response text)
and raises exceptions on errors. The wrapping into EventCallbackResult

View File

@@ -10,6 +10,7 @@ from integrations.github.github_types import (
)
from integrations.models import Message
from integrations.resolver_context import ResolverUserContext
from integrations.resolver_org_router import resolve_org_for_repo
from integrations.types import ResolverViewInterface, UserData
from integrations.utils import (
ENABLE_PROACTIVE_CONVERSATION_STARTERS,
@@ -24,9 +25,9 @@ from jinja2 import Environment
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from server.auth.token_manager import TokenManager
from server.config import get_config
from storage.database import session_maker
from storage.org_store import OrgStore
from storage.proactive_conversation_store import ProactiveConversationStore
from storage.saas_conversation_store import SaasConversationStore
from storage.saas_secrets_store import SaasSecretsStore
from openhands.agent_server.models import SendMessageRequest
@@ -42,16 +43,13 @@ from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.integrations.service_types import Comment
from openhands.sdk import TextContent
from openhands.server.services.conversation_service import (
initialize_conversation,
start_conversation,
)
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
from openhands.utils.async_utils import call_sync_from_async
from openhands.utils.conversation_summary import get_default_conversation_title
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
@@ -73,7 +71,6 @@ async def get_user_proactive_conversation_setting(user_id: str | None) -> bool:
This function checks both the global environment variable kill switch AND
the user's individual setting. Both must be true for the function to return true.
"""
# If no user ID is provided, we can't check user settings
if not user_id:
return False
@@ -82,13 +79,10 @@ async def get_user_proactive_conversation_setting(user_id: str | None) -> bool:
if not ENABLE_PROACTIVE_CONVERSATION_STARTERS:
return False
def _get_setting():
org = OrgStore.get_current_org_from_keycloak_user_id(user_id)
if not org:
return False
return bool(org.enable_proactive_conversation_starters)
return await call_sync_from_async(_get_setting)
org = await OrgStore.get_current_org_from_keycloak_user_id(user_id)
if not org:
return False
return bool(org.enable_proactive_conversation_starters)
# =================================================
@@ -153,20 +147,23 @@ class GithubIssue(ResolverViewInterface):
return user_instructions, conversation_instructions
async def _get_user_secrets(self):
secrets_store = SaasSecretsStore(
self.user_info.keycloak_user_id, session_maker, get_config()
)
secrets_store = SaasSecretsStore(self.user_info.keycloak_user_id, get_config())
user_secrets = await secrets_store.load()
return user_secrets.custom_secrets if user_secrets else None
async def initialize_new_conversation(self) -> ConversationMetadata:
# FIXME: Handle if initialize_conversation returns None
self.v1_enabled = await is_v1_enabled_for_github_resolver(
self.user_info.keycloak_user_id
)
# Resolve target org based on claimed git organizations
self.resolved_org_id = await resolve_org_for_repo(
provider='github',
full_repo_name=self.full_repo_name,
keycloak_user_id=self.user_info.keycloak_user_id,
)
logger.info(
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {self.v1_enabled}'
)
@@ -180,16 +177,28 @@ class GithubIssue(ResolverViewInterface):
selected_repository=self.full_repo_name,
)
conversation_metadata: ConversationMetadata = await initialize_conversation( # type: ignore[assignment]
user_id=self.user_info.keycloak_user_id,
conversation_id=None,
selected_repository=self.full_repo_name,
selected_branch=self._get_branch_name(),
conversation_trigger=ConversationTrigger.RESOLVER,
git_provider=ProviderType.GITHUB,
# Create the conversation store with resolver org routing
# (bypasses initialize_conversation to avoid threading enterprise-only
# resolver_org_id through the generic OSS interface)
store = await SaasConversationStore.get_resolver_instance(
get_config(),
self.user_info.keycloak_user_id,
self.resolved_org_id,
)
self.conversation_id = conversation_metadata.conversation_id
conversation_id = uuid4().hex
conversation_metadata = ConversationMetadata(
trigger=ConversationTrigger.RESOLVER,
conversation_id=conversation_id,
title=get_default_conversation_title(conversation_id),
user_id=self.user_info.keycloak_user_id,
selected_repository=self.full_repo_name,
selected_branch=self._get_branch_name(),
git_provider=ProviderType.GITHUB,
)
await store.save_metadata(conversation_metadata)
self.conversation_id = conversation_id
return conversation_metadata
async def create_new_conversation(
@@ -199,44 +208,32 @@ class GithubIssue(ResolverViewInterface):
conversation_metadata: ConversationMetadata,
saas_user_auth: UserAuth,
):
logger.info(
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {self.v1_enabled}'
# V0 conversation path has been removed - all conversations use V1 app conversation service
await self._create_v1_conversation(
jinja_env, saas_user_auth, conversation_metadata
)
if self.v1_enabled:
# Use V1 app conversation service
await self._create_v1_conversation(
jinja_env, saas_user_auth, conversation_metadata
)
else:
await self._create_v0_conversation(
jinja_env, git_provider_tokens, conversation_metadata
)
async def _create_v0_conversation(
self,
jinja_env: Environment,
git_provider_tokens: PROVIDER_TOKEN_TYPE,
conversation_metadata: ConversationMetadata,
):
"""Create conversation using the legacy V0 system."""
logger.info('[GitHub]: Creating V0 conversation')
custom_secrets = await self._get_user_secrets()
async def _get_v1_initial_user_message(self, jinja_env: Environment) -> str:
"""Build the initial user message for V1 resolver conversations.
For "issue opened" events (no specific comment body), we can simply
concatenate the user prompt and the rendered issue context.
Subclasses that represent comment-driven events (issue comments, PR review
comments, inline review comments) override this method to control ordering
(e.g., context first, then the triggering comment, then previous comments).
"""
user_instructions, conversation_instructions = await self._get_instructions(
jinja_env
)
await start_conversation(
user_id=self.user_info.keycloak_user_id,
git_provider_tokens=git_provider_tokens,
custom_secrets=custom_secrets,
initial_user_msg=user_instructions,
image_urls=None,
replay_json=None,
conversation_id=conversation_metadata.conversation_id,
conversation_metadata=conversation_metadata,
conversation_instructions=conversation_instructions,
)
parts: list[str] = []
if user_instructions.strip():
parts.append(user_instructions.strip())
if conversation_instructions.strip():
parts.append(conversation_instructions.strip())
return '\n\n'.join(parts)
async def _create_v1_conversation(
self,
@@ -247,13 +244,11 @@ class GithubIssue(ResolverViewInterface):
"""Create conversation using the new V1 app conversation system."""
logger.info('[GitHub V1]: Creating V1 conversation')
user_instructions, conversation_instructions = await self._get_instructions(
jinja_env
)
initial_user_text = await self._get_v1_initial_user_message(jinja_env)
# Create the initial message request
initial_message = SendMessageRequest(
role='user', content=[TextContent(text=user_instructions)]
role='user', content=[TextContent(text=initial_user_text)]
)
# Create the GitHub V1 callback processor
@@ -265,7 +260,9 @@ class GithubIssue(ResolverViewInterface):
# Create the V1 conversation start request with the callback processor
start_request = AppConversationStartRequest(
conversation_id=UUID(conversation_metadata.conversation_id),
system_message_suffix=conversation_instructions,
# NOTE: Resolver instructions are intended to be lower priority than the
# system prompt, so we inject them into the initial user message.
system_message_suffix=None,
initial_message=initial_message,
selected_repository=self.full_repo_name,
selected_branch=self._get_branch_name(),
@@ -278,7 +275,10 @@ class GithubIssue(ResolverViewInterface):
)
# Set up the GitHub user context for the V1 system
github_user_context = ResolverUserContext(saas_user_auth=saas_user_auth)
github_user_context = ResolverUserContext(
saas_user_auth=saas_user_auth,
resolver_org_id=self.resolved_org_id,
)
setattr(injector_state, USER_CONTEXT_ATTR, github_user_context)
async with get_app_conversation_service(
@@ -306,7 +306,7 @@ class GithubIssue(ResolverViewInterface):
'full_repo_name': self.full_repo_name,
'installation_id': self.installation_id,
},
send_summary_instruction=self.send_summary_instruction,
should_request_summary=self.send_summary_instruction,
)
@@ -336,6 +336,17 @@ class GithubIssueComment(GithubIssue):
return user_instructions, conversation_instructions
async def _get_v1_initial_user_message(self, jinja_env: Environment) -> str:
await self._load_resolver_context()
template = jinja_env.get_template('issue_comment_initial_message.j2')
return template.render(
issue_number=self.issue_number,
issue_title=self.title,
issue_body=self.description,
issue_comment=self.comment_body,
previous_comments=self.previous_comments,
).strip()
@dataclass
class GithubPRComment(GithubIssueComment):
@@ -362,6 +373,18 @@ class GithubPRComment(GithubIssueComment):
return user_instructions, conversation_instructions
async def _get_v1_initial_user_message(self, jinja_env: Environment) -> str:
await self._load_resolver_context()
template = jinja_env.get_template('pr_update_initial_message.j2')
return template.render(
pr_number=self.issue_number,
branch_name=self.branch_name,
pr_title=self.title,
pr_body=self.description,
pr_comment=self.comment_body,
comments=self.previous_comments,
).strip()
@dataclass
class GithubInlinePRComment(GithubPRComment):
@@ -408,6 +431,20 @@ class GithubInlinePRComment(GithubPRComment):
return user_instructions, conversation_instructions
async def _get_v1_initial_user_message(self, jinja_env: Environment) -> str:
await self._load_resolver_context()
template = jinja_env.get_template('pr_update_initial_message.j2')
return template.render(
pr_number=self.issue_number,
branch_name=self.branch_name,
pr_title=self.title,
pr_body=self.description,
file_location=self.file_location,
line_number=self.line_number,
pr_comment=self.comment_body,
comments=self.previous_comments,
).strip()
def _create_github_v1_callback_processor(self):
"""Create a V1 callback processor for GitHub integration."""
from integrations.github.github_v1_callback_processor import (
@@ -423,7 +460,7 @@ class GithubInlinePRComment(GithubPRComment):
'comment_id': self.comment_id,
},
inline_pr_comment=True,
send_summary_instruction=self.send_summary_instruction,
should_request_summary=self.send_summary_instruction,
)
@@ -740,7 +777,7 @@ class GithubFactory:
@staticmethod
async def create_github_view_from_payload(
message: Message, keycloak_user_id: str
) -> ResolverViewInterface:
) -> GithubViewType:
"""Create the appropriate class (GithubIssue or GithubPRComment) based on the payload.
Also return metadata about the event (e.g., action type).
"""

View File

@@ -1,4 +1,7 @@
from __future__ import annotations
from types import MappingProxyType
from typing import cast
from integrations.gitlab.gitlab_view import (
GitlabFactory,
@@ -17,10 +20,10 @@ from integrations.utils import (
OPENHANDS_RESOLVER_TEMPLATES_DIR,
get_session_expired_message,
)
from integrations.v1_utils import get_saas_user_auth
from jinja2 import Environment, FileSystemLoader
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from server.utils.conversation_callback_utils import register_callback_processor
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
@@ -33,7 +36,7 @@ from openhands.server.types import (
from openhands.storage.data_models.secrets import Secrets
class GitlabManager(Manager):
class GitlabManager(Manager[GitlabViewType]):
def __init__(self, token_manager: TokenManager, data_collector: None = None):
self.token_manager = token_manager
@@ -67,11 +70,11 @@ class GitlabManager(Manager):
logger.warning(f'Got invalid keyloak user id for GitLab User {user_id}')
return False
# Importing here prevents circular import
# GitLabServiceImpl returns SaaSGitLabService in enterprise context
from integrations.gitlab.gitlab_service import SaaSGitLabService
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
external_auth_id=keycloak_user_id
gitlab_service = cast(
SaaSGitLabService, GitLabServiceImpl(external_auth_id=keycloak_user_id)
)
return await gitlab_service.user_has_write_access(project_id)
@@ -121,66 +124,57 @@ class GitlabManager(Manager):
# Check if the user has write access to the repository
return has_write_access
async def send_message(self, message: Message, gitlab_view: ResolverViewInterface):
"""
Send a message to GitLab based on the view type.
async def send_message(self, message: str, gitlab_view: ResolverViewInterface):
"""Send a message to GitLab based on the view type.
Args:
message: The message to send
message: The message content to send (plain text string)
gitlab_view: The GitLab view object containing issue/PR/comment info
"""
keycloak_user_id = gitlab_view.user_info.keycloak_user_id
# Importing here prevents circular import
# GitLabServiceImpl returns SaaSGitLabService in enterprise context
from integrations.gitlab.gitlab_service import SaaSGitLabService
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
external_auth_id=keycloak_user_id
gitlab_service = cast(
SaaSGitLabService, GitLabServiceImpl(external_auth_id=keycloak_user_id)
)
outgoing_message = message.message
if isinstance(gitlab_view, GitlabInlineMRComment) or isinstance(
gitlab_view, GitlabMRComment
):
await gitlab_service.reply_to_mr(
gitlab_view.project_id,
gitlab_view.issue_number,
gitlab_view.discussion_id,
message.message,
project_id=str(gitlab_view.project_id),
merge_request_iid=str(gitlab_view.issue_number),
discussion_id=gitlab_view.discussion_id,
body=message,
)
elif isinstance(gitlab_view, GitlabIssueComment):
await gitlab_service.reply_to_issue(
gitlab_view.project_id,
gitlab_view.issue_number,
gitlab_view.discussion_id,
outgoing_message,
project_id=str(gitlab_view.project_id),
issue_number=str(gitlab_view.issue_number),
discussion_id=gitlab_view.discussion_id,
body=message,
)
elif isinstance(gitlab_view, GitlabIssue):
await gitlab_service.reply_to_issue(
gitlab_view.project_id,
gitlab_view.issue_number,
None, # no discussion id, issue is tagged
outgoing_message,
project_id=str(gitlab_view.project_id),
issue_number=str(gitlab_view.issue_number),
discussion_id=None, # no discussion id, issue is tagged
body=message,
)
else:
logger.warning(
f'[GitLab] Unsupported view type: {type(gitlab_view).__name__}'
)
async def start_job(self, gitlab_view: GitlabViewType):
"""
Start a job for the GitLab view.
async def start_job(self, gitlab_view: GitlabViewType) -> None:
"""Start a job for the GitLab view using V1 app conversation system.
Args:
gitlab_view: The GitLab view object containing issue/PR/comment info
"""
# Importing here prevents circular import
from server.conversation_callback_processor.gitlab_callback_processor import (
GitlabCallbackProcessor,
)
try:
try:
user_info = gitlab_view.user_info
@@ -214,8 +208,18 @@ class GitlabManager(Manager):
)
)
# Initialize conversation and get metadata (following GitHub pattern)
convo_metadata = await gitlab_view.initialize_new_conversation()
saas_user_auth = await get_saas_user_auth(
gitlab_view.user_info.keycloak_user_id, self.token_manager
)
await gitlab_view.create_new_conversation(
self.jinja_env, secret_store.provider_tokens
self.jinja_env,
secret_store.provider_tokens,
convo_metadata,
saas_user_auth,
)
conversation_id = gitlab_view.conversation_id
@@ -224,18 +228,7 @@ class GitlabManager(Manager):
f'[GitLab] Created conversation {conversation_id} for user {user_info.username}'
)
# Create a GitlabCallbackProcessor for this conversation
processor = GitlabCallbackProcessor(
gitlab_view=gitlab_view,
send_summary_instruction=True,
)
# Register the callback processor
register_callback_processor(conversation_id, processor)
logger.info(
f'[GitLab] Created callback processor for conversation {conversation_id}'
)
# V1 callback processors are registered by the view during conversation creation
conversation_link = CONVERSATION_URL.format(conversation_id)
msg_info = f"I'm on it! {user_info.username} can [track my progress at all-hands.dev]({conversation_link})"
@@ -262,12 +255,10 @@ class GitlabManager(Manager):
msg_info = get_session_expired_message(user_info.username)
# Send the acknowledgment message
msg = self.create_outgoing_message(msg_info)
await self.send_message(msg, gitlab_view)
await self.send_message(msg_info, gitlab_view)
except Exception as e:
logger.exception(f'[GitLab] Error starting job: {str(e)}')
msg = self.create_outgoing_message(
msg='Uh oh! There was an unexpected error starting the job :('
await self.send_message(
'Uh oh! There was an unexpected error starting the job :(', gitlab_view
)
await self.send_message(msg, gitlab_view)

View File

@@ -185,6 +185,30 @@ class SaaSGitLabService(GitLabService):
users_personal_projects: List of personal projects owned by the user
repositories: List of Repository objects to store
"""
# If external_auth_id is not set, try to determine it from the Keycloak token
if not self.external_auth_id and self.external_auth_token:
try:
user_info = await self.token_manager.get_user_info(
self.external_auth_token.get_secret_value()
)
keycloak_user_id = user_info.sub
self.external_auth_id = keycloak_user_id
logger.info(
f'Determined external_auth_id from Keycloak token: {self.external_auth_id}'
)
except Exception:
logger.warning(
'Cannot store repository data: external_auth_id is not set and could not be determined from token',
exc_info=True,
)
return
if not self.external_auth_id:
logger.warning(
'Cannot store repository data: external_auth_id could not be determined'
)
return
try:
# First, add owned projects and groups to the database
await self.add_owned_projects_and_groups_to_db(users_personal_projects)

View File

@@ -0,0 +1,274 @@
import logging
from typing import Any
from uuid import UUID
import httpx
from integrations.utils import get_summary_instruction
from integrations.v1_utils import handle_callback_error
from pydantic import Field
from openhands.agent_server.models import AskAgentRequest, AskAgentResponse
from openhands.app_server.event_callback.event_callback_models import (
EventCallback,
EventCallbackProcessor,
)
from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResult,
EventCallbackResultStatus,
)
from openhands.app_server.event_callback.util import (
ensure_conversation_found,
ensure_running_sandbox,
get_agent_server_url_from_sandbox,
)
from openhands.sdk import Event
from openhands.sdk.event import ConversationStateUpdateEvent
_logger = logging.getLogger(__name__)
class GitlabV1CallbackProcessor(EventCallbackProcessor):
"""Callback processor for GitLab V1 integrations."""
gitlab_view_data: dict[str, Any] = Field(default_factory=dict)
should_request_summary: bool = Field(default=True)
inline_mr_comment: bool = Field(default=False)
async def __call__(
self,
conversation_id: UUID,
callback: EventCallback,
event: Event,
) -> EventCallbackResult | None:
"""Process events for GitLab V1 integration."""
# Only handle ConversationStateUpdateEvent for execution_status
if not isinstance(event, ConversationStateUpdateEvent):
return None
if event.key != 'execution_status':
return None
# Log ALL terminal states for monitoring (finished, error, stuck)
_logger.info('[GitLab V1] Callback agent state was %s', event)
# Only request summary when execution has finished successfully
if event.value != 'finished':
return None
_logger.info(
'[GitLab V1] Should request summary: %s', self.should_request_summary
)
if not self.should_request_summary:
return None
self.should_request_summary = False
try:
_logger.info(f'[GitLab V1] Requesting summary {conversation_id}')
summary = await self._request_summary(conversation_id)
_logger.info(
f'[GitLab V1] Posting summary {conversation_id}',
extra={'summary': summary},
)
await self._post_summary_to_gitlab(summary)
return EventCallbackResult(
status=EventCallbackResultStatus.SUCCESS,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=summary,
)
except Exception as e:
can_post_error = bool(self.gitlab_view_data.get('keycloak_user_id'))
await handle_callback_error(
error=e,
conversation_id=conversation_id,
service_name='GitLab',
service_logger=_logger,
can_post_error=can_post_error,
post_error_func=self._post_summary_to_gitlab,
)
return EventCallbackResult(
status=EventCallbackResultStatus.ERROR,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=str(e),
)
# -------------------------------------------------------------------------
# GitLab helpers
# -------------------------------------------------------------------------
async def _post_summary_to_gitlab(self, summary: str) -> None:
"""Post a summary comment to the configured GitLab issue or MR."""
# Import here to avoid circular imports
from integrations.gitlab.gitlab_service import SaaSGitLabService
keycloak_user_id = self.gitlab_view_data.get('keycloak_user_id')
if not keycloak_user_id:
raise RuntimeError('Missing keycloak user ID for GitLab')
gitlab_service = SaaSGitLabService(external_auth_id=keycloak_user_id)
project_id = self.gitlab_view_data['project_id']
issue_number = self.gitlab_view_data['issue_number']
discussion_id = self.gitlab_view_data['discussion_id']
is_mr = self.gitlab_view_data.get('is_mr', False)
if is_mr:
await gitlab_service.reply_to_mr(
project_id,
issue_number,
discussion_id,
summary,
)
else:
await gitlab_service.reply_to_issue(
project_id,
issue_number,
discussion_id,
summary,
)
# -------------------------------------------------------------------------
# Agent / sandbox helpers
# -------------------------------------------------------------------------
async def _ask_question(
self,
httpx_client: httpx.AsyncClient,
agent_server_url: str,
conversation_id: UUID,
session_api_key: str,
message_content: str,
) -> str:
"""Send a message to the agent server via the V1 API and return response text."""
send_message_request = AskAgentRequest(question=message_content)
url = (
f"{agent_server_url.rstrip('/')}"
f"/api/conversations/{conversation_id}/ask_agent"
)
headers = {'X-Session-API-Key': session_api_key}
payload = send_message_request.model_dump()
try:
response = await httpx_client.post(
url,
json=payload,
headers=headers,
timeout=30.0,
)
response.raise_for_status()
agent_response = AskAgentResponse.model_validate(response.json())
return agent_response.response
except httpx.HTTPStatusError as e:
error_detail = f'HTTP {e.response.status_code} error'
try:
error_body = e.response.text
if error_body:
error_detail += f': {error_body}'
except Exception: # noqa: BLE001
pass
_logger.error(
'[GitLab V1] HTTP error sending message to %s: %s. '
'Request payload: %s. Response headers: %s',
url,
error_detail,
payload,
dict(e.response.headers),
exc_info=True,
)
raise Exception(f'Failed to send message to agent server: {error_detail}')
except httpx.TimeoutException:
error_detail = f'Request timeout after 30 seconds to {url}'
_logger.error(
'[GitLab V1] %s. Request payload: %s',
error_detail,
payload,
exc_info=True,
)
raise Exception(error_detail)
except httpx.RequestError as e:
error_detail = f'Request error to {url}: {str(e)}'
_logger.error(
'[GitLab V1] %s. Request payload: %s',
error_detail,
payload,
exc_info=True,
)
raise Exception(error_detail)
# -------------------------------------------------------------------------
# Summary orchestration
# -------------------------------------------------------------------------
async def _request_summary(self, conversation_id: UUID) -> str:
"""Ask the agent to produce a summary of its work and return the agent response.
NOTE: This method now returns a string (the agent server's response text)
and raises exceptions on errors. The wrapping into EventCallbackResult
is handled by __call__.
"""
# Import services within the method to avoid circular imports
from openhands.app_server.config import (
get_app_conversation_info_service,
get_httpx_client,
get_sandbox_service,
)
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import (
ADMIN,
USER_CONTEXT_ATTR,
)
# Create injector state for dependency injection
state = InjectorState()
setattr(state, USER_CONTEXT_ATTR, ADMIN)
async with (
get_app_conversation_info_service(state) as app_conversation_info_service,
get_sandbox_service(state) as sandbox_service,
get_httpx_client(state) as httpx_client,
):
# 1. Conversation lookup
app_conversation_info = ensure_conversation_found(
await app_conversation_info_service.get_app_conversation_info(
conversation_id
),
conversation_id,
)
# 2. Sandbox lookup + validation
sandbox = ensure_running_sandbox(
await sandbox_service.get_sandbox(app_conversation_info.sandbox_id),
app_conversation_info.sandbox_id,
)
assert (
sandbox.session_api_key is not None
), f'No session API key for sandbox: {sandbox.id}'
# 3. URL + instruction
agent_server_url = get_agent_server_url_from_sandbox(sandbox)
# Prepare message based on agent state
message_content = get_summary_instruction()
# Ask the agent and return the response text
return await self._ask_question(
httpx_client=httpx_client,
agent_server_url=agent_server_url,
conversation_id=conversation_id,
session_api_key=sandbox.session_api_key,
message_content=message_content,
)

View File

@@ -1,25 +1,52 @@
from dataclasses import dataclass
from uuid import UUID, uuid4
from integrations.models import Message
from integrations.resolver_context import ResolverUserContext
from integrations.resolver_org_router import resolve_org_for_repo
from integrations.types import ResolverViewInterface, UserData
from integrations.utils import HOST, get_oh_labels, has_exact_mention
from integrations.utils import (
ENABLE_V1_GITLAB_RESOLVER,
HOST,
get_oh_labels,
get_user_v1_enabled_setting,
has_exact_mention,
)
from jinja2 import Environment
from server.auth.token_manager import TokenManager
from server.config import get_config
from storage.database import session_maker
from storage.saas_conversation_store import SaasConversationStore
from storage.saas_secrets_store import SaasSecretsStore
from openhands.agent_server.models import SendMessageRequest
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartRequest,
AppConversationStartTaskStatus,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
from openhands.integrations.service_types import Comment
from openhands.server.services.conversation_service import create_new_conversation
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
from openhands.utils.conversation_summary import get_default_conversation_title
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
CONFIDENTIAL_NOTE = 'confidential_note'
NOTE_TYPES = ['note', CONFIDENTIAL_NOTE]
async def is_v1_enabled_for_gitlab_resolver(user_id: str) -> bool:
return await get_user_v1_enabled_setting(user_id) and ENABLE_V1_GITLAB_RESOLVER
# =================================================
# SECTION: Factory to create appriorate Gitlab view
# =================================================
@@ -41,6 +68,10 @@ class GitlabIssue(ResolverViewInterface):
description: str
previous_comments: list[Comment]
is_mr: bool
v1_enabled: bool
def _get_branch_name(self) -> str | None:
return getattr(self, 'branch_name', None)
async def _load_resolver_context(self):
gitlab_service = GitLabServiceImpl(
@@ -78,35 +109,149 @@ class GitlabIssue(ResolverViewInterface):
return user_instructions, conversation_instructions
async def _get_user_secrets(self):
secrets_store = SaasSecretsStore(
self.user_info.keycloak_user_id, session_maker, get_config()
)
secrets_store = SaasSecretsStore(self.user_info.keycloak_user_id, get_config())
user_secrets = await secrets_store.load()
return user_secrets.custom_secrets if user_secrets else None
async def initialize_new_conversation(self) -> ConversationMetadata:
# v1_enabled is already set at construction time in the factory method
# This is the source of truth for the conversation type
# Resolve target org based on claimed git organizations
self.resolved_org_id = await resolve_org_for_repo(
provider='gitlab',
full_repo_name=self.full_repo_name,
keycloak_user_id=self.user_info.keycloak_user_id,
)
if self.v1_enabled:
# Create dummy conversation metadata
# Don't save to conversation store
# V1 conversations are stored in a separate table
self.conversation_id = uuid4().hex
return ConversationMetadata(
conversation_id=self.conversation_id,
selected_repository=self.full_repo_name,
)
# Create the conversation store with resolver org routing
# (bypasses initialize_conversation to avoid threading enterprise-only
# resolver_org_id through the generic OSS interface)
store = await SaasConversationStore.get_resolver_instance(
get_config(),
self.user_info.keycloak_user_id,
self.resolved_org_id,
)
conversation_id = uuid4().hex
conversation_metadata = ConversationMetadata(
trigger=ConversationTrigger.RESOLVER,
conversation_id=conversation_id,
title=get_default_conversation_title(conversation_id),
user_id=self.user_info.keycloak_user_id,
selected_repository=self.full_repo_name,
selected_branch=self._get_branch_name(),
git_provider=ProviderType.GITLAB,
)
await store.save_metadata(conversation_metadata)
self.conversation_id = conversation_id
return conversation_metadata
async def create_new_conversation(
self, jinja_env: Environment, git_provider_tokens: PROVIDER_TOKEN_TYPE
self,
jinja_env: Environment,
git_provider_tokens: PROVIDER_TOKEN_TYPE,
conversation_metadata: ConversationMetadata,
saas_user_auth: UserAuth,
):
custom_secrets = await self._get_user_secrets()
# V0 conversation path has been removed - all conversations use V1 app conversation service
await self._create_v1_conversation(
jinja_env, saas_user_auth, conversation_metadata
)
async def _create_v1_conversation(
self,
jinja_env: Environment,
saas_user_auth: UserAuth,
conversation_metadata: ConversationMetadata,
):
"""Create conversation using the new V1 app conversation system."""
logger.info('[GitLab V1]: Creating V1 conversation')
user_instructions, conversation_instructions = await self._get_instructions(
jinja_env
)
agent_loop_info = await create_new_conversation(
user_id=self.user_info.keycloak_user_id,
git_provider_tokens=git_provider_tokens,
custom_secrets=custom_secrets,
selected_repository=self.full_repo_name,
selected_branch=None,
initial_user_msg=user_instructions,
conversation_instructions=conversation_instructions,
image_urls=None,
conversation_trigger=ConversationTrigger.RESOLVER,
replay_json=None,
# Create the initial message request
initial_message = SendMessageRequest(
role='user', content=[TextContent(text=user_instructions)]
)
# Create the GitLab V1 callback processor
gitlab_callback_processor = self._create_gitlab_v1_callback_processor()
# Get the app conversation service and start the conversation
injector_state = InjectorState()
# Determine the title based on whether it's an MR or issue
title_prefix = 'GitLab MR' if self.is_mr else 'GitLab Issue'
title = f'{title_prefix} #{self.issue_number}: {self.title}'
# Create the V1 conversation start request with the callback processor
start_request = AppConversationStartRequest(
conversation_id=UUID(conversation_metadata.conversation_id),
system_message_suffix=conversation_instructions,
initial_message=initial_message,
selected_repository=self.full_repo_name,
selected_branch=self._get_branch_name(),
git_provider=ProviderType.GITLAB,
title=title,
trigger=ConversationTrigger.RESOLVER,
processors=[
gitlab_callback_processor
], # Pass the callback processor directly
)
# Set up the GitLab user context for the V1 system
gitlab_user_context = ResolverUserContext(
saas_user_auth=saas_user_auth,
resolver_org_id=self.resolved_org_id,
)
setattr(injector_state, USER_CONTEXT_ATTR, gitlab_user_context)
async with get_app_conversation_service(
injector_state
) as app_conversation_service:
async for task in app_conversation_service.start_app_conversation(
start_request
):
if task.status == AppConversationStartTaskStatus.ERROR:
logger.error(f'Failed to start V1 conversation: {task.detail}')
raise RuntimeError(
f'Failed to start V1 conversation: {task.detail}'
)
def _create_gitlab_v1_callback_processor(self):
"""Create a V1 callback processor for GitLab integration."""
from integrations.gitlab.gitlab_v1_callback_processor import (
GitlabV1CallbackProcessor,
)
# Create and return the GitLab V1 callback processor
return GitlabV1CallbackProcessor(
gitlab_view_data={
'issue_number': self.issue_number,
'project_id': self.project_id,
'full_repo_name': self.full_repo_name,
'installation_id': self.installation_id,
'keycloak_user_id': self.user_info.keycloak_user_id,
'is_mr': self.is_mr,
'discussion_id': getattr(self, 'discussion_id', None),
},
should_request_summary=self.send_summary_instruction,
)
self.conversation_id = agent_loop_info.conversation_id
return self.conversation_id
@dataclass
@@ -141,6 +286,9 @@ class GitlabIssueComment(GitlabIssue):
class GitlabMRComment(GitlabIssueComment):
branch_name: str
def _get_branch_name(self) -> str | None:
return self.branch_name
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
user_instructions_template = jinja_env.get_template('mr_update_prompt.j2')
await self._load_resolver_context()
@@ -162,29 +310,6 @@ class GitlabMRComment(GitlabIssueComment):
return user_instructions, conversation_instructions
async def create_new_conversation(
self, jinja_env: Environment, git_provider_tokens: PROVIDER_TOKEN_TYPE
):
custom_secrets = await self._get_user_secrets()
user_instructions, conversation_instructions = await self._get_instructions(
jinja_env
)
agent_loop_info = await create_new_conversation(
user_id=self.user_info.keycloak_user_id,
git_provider_tokens=git_provider_tokens,
custom_secrets=custom_secrets,
selected_repository=self.full_repo_name,
selected_branch=self.branch_name,
initial_user_msg=user_instructions,
conversation_instructions=conversation_instructions,
image_urls=None,
conversation_trigger=ConversationTrigger.RESOLVER,
replay_json=None,
)
self.conversation_id = agent_loop_info.conversation_id
return self.conversation_id
@dataclass
class GitlabInlineMRComment(GitlabMRComment):
@@ -306,7 +431,7 @@ class GitlabFactory:
@staticmethod
async def create_gitlab_view_from_payload(
message: Message, token_manager: TokenManager
) -> ResolverViewInterface:
) -> GitlabViewType:
payload = message.message['payload']
installation_id = message.message['installation_id']
user = payload['user']
@@ -325,6 +450,16 @@ class GitlabFactory:
user_id=user_id, username=username, keycloak_user_id=keycloak_user_id
)
# Check v1_enabled at construction time - this is the source of truth
v1_enabled = (
await is_v1_enabled_for_gitlab_resolver(keycloak_user_id)
if keycloak_user_id
else False
)
logger.info(
f'[GitLab V1]: User flag found for {keycloak_user_id} is {v1_enabled}'
)
if GitlabFactory.is_labeled_issue(message):
issue_iid = payload['object_attributes']['iid']
@@ -346,6 +481,7 @@ class GitlabFactory:
description='',
previous_comments=[],
is_mr=False,
v1_enabled=v1_enabled,
)
elif GitlabFactory.is_issue_comment(message):
@@ -376,6 +512,7 @@ class GitlabFactory:
description='',
previous_comments=[],
is_mr=False,
v1_enabled=v1_enabled,
)
elif GitlabFactory.is_mr_comment(message):
@@ -408,6 +545,7 @@ class GitlabFactory:
description='',
previous_comments=[],
is_mr=True,
v1_enabled=v1_enabled,
)
elif GitlabFactory.is_mr_comment(message, inline=True):
@@ -448,4 +586,7 @@ class GitlabFactory:
description='',
previous_comments=[],
is_mr=True,
v1_enabled=v1_enabled,
)
raise ValueError(f'Unhandled GitLab webhook event: {message}')

View File

@@ -4,7 +4,9 @@ This module contains reusable functions and classes for installing GitLab webhoo
that can be used by both the cron job and API routes.
"""
from typing import cast
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import uuid4
from integrations.types import GitLabResourceType
@@ -13,7 +15,9 @@ from storage.gitlab_webhook import GitlabWebhook, WebhookStatus
from storage.gitlab_webhook_store import GitlabWebhookStore
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import GitService
if TYPE_CHECKING:
from integrations.gitlab.gitlab_service import SaaSGitLabService
# Webhook configuration constants
WEBHOOK_NAME = 'OpenHands Resolver'
@@ -35,7 +39,7 @@ class BreakLoopException(Exception):
async def verify_webhook_conditions(
gitlab_service: type[GitService],
gitlab_service: SaaSGitLabService,
resource_type: GitLabResourceType,
resource_id: str,
webhook_store: GitlabWebhookStore,
@@ -52,10 +56,6 @@ async def verify_webhook_conditions(
webhook_store: Webhook store instance
webhook: Webhook object to verify
"""
from integrations.gitlab.gitlab_service import SaaSGitLabService
gitlab_service = cast(type[SaaSGitLabService], gitlab_service)
# Check if resource exists
does_resource_exist, status = await gitlab_service.check_resource_exists(
resource_type, resource_id
@@ -106,7 +106,9 @@ async def verify_webhook_conditions(
does_webhook_exist_on_resource,
status,
) = await gitlab_service.check_webhook_exists_on_resource(
resource_type, resource_id, GITLAB_WEBHOOK_URL
resource_type=resource_type,
resource_id=resource_id,
webhook_url=GITLAB_WEBHOOK_URL,
)
logger.info(
@@ -131,7 +133,7 @@ async def verify_webhook_conditions(
async def install_webhook_on_resource(
gitlab_service: type[GitService],
gitlab_service: SaaSGitLabService,
resource_type: GitLabResourceType,
resource_id: str,
webhook_store: GitlabWebhookStore,
@@ -150,10 +152,6 @@ async def install_webhook_on_resource(
Returns:
Tuple of (webhook_id, status)
"""
from integrations.gitlab.gitlab_service import SaaSGitLabService
gitlab_service = cast(type[SaaSGitLabService], gitlab_service)
webhook_secret = f'{webhook.user_id}-{str(uuid4())}'
webhook_uuid = f'{str(uuid4())}'
@@ -167,17 +165,15 @@ async def install_webhook_on_resource(
scopes=SCOPES,
)
logger.info(
'Creating new webhook',
extra={
'webhook_id': webhook_id,
'status': status,
'resource_id': resource_id,
'resource_type': resource_type,
},
)
log_extra = {
'webhook_id': webhook_id,
'status': status,
'resource_id': resource_id,
'resource_type': resource_type,
}
if status == WebhookStatus.RATE_LIMITED:
logger.warning('Rate limited while creating webhook', extra=log_extra)
raise BreakLoopException()
if webhook_id:
@@ -191,9 +187,8 @@ async def install_webhook_on_resource(
'webhook_uuid': webhook_uuid, # required to identify which webhook installation is sending payload
},
)
logger.info(
f'Installed webhook for {webhook.user_id} on {resource_type}:{resource_id}'
)
logger.info('Created new webhook', extra=log_extra)
else:
logger.error('Failed to create webhook', extra=log_extra)
return webhook_id, status

View File

@@ -24,20 +24,20 @@ from integrations.jira.jira_types import (
RepositoryNotFoundError,
StartingConvoException,
)
from integrations.jira.jira_view import JiraFactory, JiraNewConversationView
from integrations.jira.jira_view import JiraFactory
from integrations.manager import Manager
from integrations.models import Message
from integrations.utils import (
HOST,
HOST_URL,
OPENHANDS_RESOLVER_TEMPLATES_DIR,
format_jira_comment_body,
get_oh_labels,
get_session_expired_message,
)
from jinja2 import Environment, FileSystemLoader
from server.auth.saas_user_auth import get_user_auth_from_keycloak_id
from server.auth.token_manager import TokenManager
from server.utils.conversation_callback_utils import register_callback_processor
from storage.jira_integration_store import JiraIntegrationStore
from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
@@ -57,7 +57,7 @@ JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
class JiraManager(Manager):
class JiraManager(Manager[JiraViewInterface]):
"""Manager for processing Jira webhook events.
This class orchestrates the flow from webhook receipt to conversation creation,
@@ -257,13 +257,8 @@ class JiraManager(Manager):
return jira_user, saas_user_auth
async def start_job(self, view: JiraViewInterface):
async def start_job(self, view: JiraViewInterface) -> None:
"""Start a Jira job/conversation."""
# Import here to prevent circular import
from server.conversation_callback_processor.jira_callback_processor import (
JiraCallbackProcessor,
)
try:
logger.info(
'[Jira] Starting job',
@@ -285,19 +280,7 @@ class JiraManager(Manager):
},
)
# Register callback processor for updates
if isinstance(view, JiraNewConversationView):
processor = JiraCallbackProcessor(
issue_key=view.payload.issue_key,
workspace_name=view.jira_workspace.name,
)
register_callback_processor(conversation_id, processor)
logger.info(
'[Jira] Callback processor registered',
extra={'conversation_id': conversation_id},
)
# Send success response
# Create success message
msg_info = view.get_response_msg()
except MissingSettingsError as e:
@@ -341,17 +324,25 @@ class JiraManager(Manager):
async def send_message(
self,
message: Message,
message: str,
issue_key: str,
jira_cloud_id: str,
svc_acc_email: str,
svc_acc_api_key: str,
):
"""Send a comment to a Jira issue."""
"""Send a comment to a Jira issue.
Args:
message: The message content to send (plain text string)
issue_key: The Jira issue key (e.g., 'PROJ-123')
jira_cloud_id: The Jira Cloud ID
svc_acc_email: Service account email for authentication
svc_acc_api_key: Service account API key for authentication
"""
url = (
f'{JIRA_CLOUD_API_URL}/{jira_cloud_id}/rest/api/2/issue/{issue_key}/comment'
)
data = {'body': message.message}
data = format_jira_comment_body(message)
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(
url, auth=(svc_acc_email, svc_acc_api_key), json=data
@@ -366,7 +357,7 @@ class JiraManager(Manager):
view.jira_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(msg=msg),
msg,
issue_key=view.payload.issue_key,
jira_cloud_id=view.jira_workspace.jira_cloud_id,
svc_acc_email=view.jira_workspace.svc_acc_email,
@@ -388,7 +379,7 @@ class JiraManager(Manager):
try:
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
await self.send_message(
self.create_outgoing_message(msg=error_msg),
error_msg,
issue_key=payload.issue_key,
jira_cloud_id=workspace.jira_cloud_id,
svc_acc_email=workspace.svc_acc_email,

View File

@@ -136,11 +136,10 @@ class JiraPayloadParser:
items = changelog.get('items', [])
# Extract labels that were added
labels = [
item.get('toString', '')
for item in items
if item.get('field') == 'labels' and 'toString' in item
]
labels = set()
for item in items:
if item.get('field') == 'labels' and item.get('toString'):
labels.update(item['toString'].split())
if self.oh_label not in labels:
return JiraPayloadSkipped(
@@ -212,8 +211,6 @@ class JiraPayloadParser:
missing.append('issue.id')
if not issue_key:
missing.append('issue.key')
if not user_email:
missing.append('user.emailAddress')
if not display_name:
missing.append('user.displayName')
if not account_id:

View File

@@ -0,0 +1,238 @@
import logging
from uuid import UUID
import httpx
from integrations.utils import format_jira_comment_body, get_summary_instruction
from pydantic import Field
from openhands.agent_server.models import AskAgentRequest, AskAgentResponse
from openhands.app_server.event_callback.event_callback_models import (
EventCallback,
EventCallbackProcessor,
)
from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResult,
EventCallbackResultStatus,
)
from openhands.app_server.event_callback.util import (
ensure_conversation_found,
ensure_running_sandbox,
get_agent_server_url_from_sandbox,
)
from openhands.sdk import Event
from openhands.sdk.event import ConversationStateUpdateEvent
from openhands.utils.http_session import httpx_verify_option
_logger = logging.getLogger(__name__)
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
class JiraV1CallbackProcessor(EventCallbackProcessor):
"""Callback processor for Jira V1 integrations."""
should_request_summary: bool = Field(default=True)
svc_acc_email: str
decrypted_api_key: str
issue_key: str
jira_cloud_id: str
async def __call__(
self,
conversation_id: UUID,
callback: EventCallback,
event: Event,
) -> EventCallbackResult | None:
"""Process events for Jira V1 integration."""
# Only handle ConversationStateUpdateEvent for execution_status
if not isinstance(event, ConversationStateUpdateEvent):
return None
if event.key != 'execution_status':
return None
_logger.info('[Jira] Callback agent state was %s', event)
# Only request summary when execution has finished successfully
if event.value != 'finished':
return None
_logger.info('[Jira] Should request summary: %s', self.should_request_summary)
if not self.should_request_summary:
return None
self.should_request_summary = False
try:
_logger.info(f'[Jira] Requesting summary {conversation_id}')
summary = await self._request_summary(conversation_id)
_logger.info(
f'[Jira] Posting summary {conversation_id}',
extra={'summary': summary},
)
await self._post_summary_to_jira(summary)
return EventCallbackResult(
status=EventCallbackResultStatus.SUCCESS,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=summary,
)
except Exception as e:
_logger.exception(f'[Jira] Failed to post summary: {e}', stack_info=True)
return EventCallbackResult(
status=EventCallbackResultStatus.ERROR,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=str(e),
)
async def _request_summary(self, conversation_id: UUID) -> str:
"""Ask the agent to produce a summary of its work and return the agent response."""
# Import services within the method to avoid circular imports
from openhands.app_server.config import (
get_app_conversation_info_service,
get_httpx_client,
get_sandbox_service,
)
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import (
ADMIN,
USER_CONTEXT_ATTR,
)
# Create injector state for dependency injection
state = InjectorState()
setattr(state, USER_CONTEXT_ATTR, ADMIN)
async with (
get_app_conversation_info_service(state) as app_conversation_info_service,
get_sandbox_service(state) as sandbox_service,
get_httpx_client(state) as httpx_client,
):
# 1. Conversation lookup
app_conversation_info = ensure_conversation_found(
await app_conversation_info_service.get_app_conversation_info(
conversation_id
),
conversation_id,
)
# 2. Sandbox lookup + validation
sandbox = ensure_running_sandbox(
await sandbox_service.get_sandbox(app_conversation_info.sandbox_id),
app_conversation_info.sandbox_id,
)
assert (
sandbox.session_api_key is not None
), f'No session API key for sandbox: {sandbox.id}'
# 3. URL + instruction
agent_server_url = get_agent_server_url_from_sandbox(sandbox)
# Prepare message based on agent state
message_content = get_summary_instruction()
# Ask the agent and return the response text
return await self._ask_question(
httpx_client=httpx_client,
agent_server_url=agent_server_url,
conversation_id=conversation_id,
session_api_key=sandbox.session_api_key,
message_content=message_content,
)
async def _ask_question(
self,
httpx_client: httpx.AsyncClient,
agent_server_url: str,
conversation_id: UUID,
session_api_key: str,
message_content: str,
) -> str:
"""Send a message to the agent server via the V1 API and return response text."""
send_message_request = AskAgentRequest(question=message_content)
url = (
f"{agent_server_url.rstrip('/')}"
f"/api/conversations/{conversation_id}/ask_agent"
)
headers = {'X-Session-API-Key': session_api_key}
payload = send_message_request.model_dump()
try:
response = await httpx_client.post(
url,
json=payload,
headers=headers,
timeout=30.0,
)
response.raise_for_status()
agent_response = AskAgentResponse.model_validate(response.json())
return agent_response.response
except httpx.HTTPStatusError as e:
error_detail = f'HTTP {e.response.status_code} error'
try:
error_body = e.response.text
if error_body:
error_detail += f': {error_body}'
except Exception:
pass
_logger.exception(
'[Jira] HTTP error sending message to %s: %s. '
'Request payload: %s. Response headers: %s',
url,
error_detail,
payload,
dict(e.response.headers),
stack_info=True,
)
raise Exception(f'Failed to send message to agent server: {error_detail}')
except httpx.TimeoutException:
error_detail = f'Request timeout after 30 seconds to {url}'
_logger.exception(
'[Jira] Timeout error: %s. Request payload: %s',
error_detail,
payload,
stack_info=True,
)
raise Exception(f'Failed to send message to agent server: {error_detail}')
async def _post_summary_to_jira(self, summary: str):
"""Post the summary back to the Jira issue."""
if not all(
[
self.svc_acc_email,
self.decrypted_api_key,
self.issue_key,
self.jira_cloud_id,
]
):
_logger.warning('[Jira] Missing required data for posting summary')
return
# Add a comment to the Jira issue with the summary
comment_url = (
f'{JIRA_CLOUD_API_URL}/{self.jira_cloud_id}'
f'/rest/api/2/issue/{self.issue_key}/comment'
)
message = f'OpenHands resolved this issue:\n\n{summary}'
comment_body = format_jira_comment_body(message)
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(
comment_url,
auth=(self.svc_acc_email, self.decrypted_api_key),
json=comment_body,
)
response.raise_for_status()
_logger.info(f'[Jira] Posted summary to {self.issue_key}')

View File

@@ -7,6 +7,7 @@ Views are responsible for:
"""
from dataclasses import dataclass, field
from uuid import UUID, uuid4
import httpx
from integrations.jira.jira_payload import JiraWebhookPayload
@@ -15,18 +16,37 @@ from integrations.jira.jira_types import (
RepositoryNotFoundError,
StartingConvoException,
)
from integrations.utils import CONVERSATION_URL, infer_repo_from_message
from integrations.jira.jira_v1_callback_processor import (
JiraV1CallbackProcessor,
)
from integrations.resolver_context import ResolverUserContext
from integrations.resolver_org_router import resolve_org_for_repo
from integrations.utils import (
CONVERSATION_URL,
infer_repo_from_message,
)
from jinja2 import Environment
from storage.jira_conversation import JiraConversation
from storage.jira_integration_store import JiraIntegrationStore
from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
from openhands.agent_server.models import SendMessageRequest
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartRequest,
AppConversationStartTaskStatus,
)
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.server.services.conversation_service import create_new_conversation
from openhands.integrations.provider import ProviderHandler, ProviderType
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
from openhands.utils.http_session import httpx_verify_option
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
@@ -46,7 +66,7 @@ class JiraNewConversationView(JiraViewInterface):
saas_user_auth: UserAuth
jira_user: JiraUser
jira_workspace: JiraWorkspace
selected_repo: str | None = None
selected_repo: str = ''
conversation_id: str = ''
# Lazy-loaded issue details (cached after first fetch)
@@ -56,6 +76,9 @@ class JiraNewConversationView(JiraViewInterface):
# Decrypted API key (set by factory)
_decrypted_api_key: str = field(default='', repr=False)
# Resolved org ID for V1 conversations
resolved_org_id: UUID | None = None
async def get_issue_details(self) -> tuple[str, str]:
"""Fetch issue details from Jira API (cached after first call).
@@ -161,56 +184,131 @@ class JiraNewConversationView(JiraViewInterface):
if not self.selected_repo:
raise StartingConvoException('No repository selected for this conversation')
jira_conversation = JiraConversation(
conversation_id=self.conversation_id,
issue_id=self.payload.issue_id,
issue_key=self.payload.issue_key,
jira_user_id=self.jira_user.id,
)
await integration_store.create_conversation(jira_conversation)
conversation_metadata = await self._create_v1_metadata()
await self._create_v1_conversation(jinja_env, conversation_metadata)
return self.conversation_id
async def _create_v1_metadata(self) -> ConversationMetadata:
"""Create conversation metadata for V1 conversations.
The JiraConversation mapping is saved to the integration store (above), but
V1 conversation metadata is managed by the app conversation system, not
the legacy conversation store.
"""
logger.info('[Jira]: Creating V1 metadata')
# Generate a dummy conversation for V1 (not saved to store)
self.conversation_id = uuid4().hex
self.resolved_org_id = await self._get_resolved_org_id()
return ConversationMetadata(
conversation_id=self.conversation_id,
selected_repository=self.selected_repo,
)
async def _create_v1_conversation(
self,
jinja_env: Environment,
conversation_metadata: ConversationMetadata,
):
"""Create conversation using the new V1 app conversation system."""
logger.info('[Jira]: Creating V1 conversation')
initial_user_text = await self._get_v1_initial_user_message(jinja_env)
# Create the initial message request
initial_message = SendMessageRequest(
role='user', content=[TextContent(text=initial_user_text)]
)
# Create the Jira V1 callback processor
jira_callback_processor = self._create_jira_v1_callback_processor()
injector_state = InjectorState()
# Create the V1 conversation start request
start_request = AppConversationStartRequest(
conversation_id=UUID(conversation_metadata.conversation_id),
system_message_suffix=None,
initial_message=initial_message,
selected_repository=self.selected_repo,
selected_branch=None,
git_provider=ProviderType.GITHUB,
title=f'Jira Issue {self.payload.issue_key}: {self._issue_title or "Unknown"}',
trigger=ConversationTrigger.JIRA,
processors=[jira_callback_processor],
)
# Set up the Jira user context for the V1 system
jira_user_context = ResolverUserContext(
saas_user_auth=self.saas_user_auth,
resolver_org_id=self.resolved_org_id,
)
setattr(injector_state, USER_CONTEXT_ATTR, jira_user_context)
async with get_app_conversation_service(
injector_state
) as app_conversation_service:
async for task in app_conversation_service.start_app_conversation(
start_request
):
if task.status == AppConversationStartTaskStatus.ERROR:
logger.error(f'Failed to start V1 conversation: {task.detail}')
raise RuntimeError(
f'Failed to start V1 conversation: {task.detail}'
)
async def _get_v1_initial_user_message(self, jinja_env: Environment) -> str:
"""Build the initial user message for V1 resolver conversations."""
issue_title, issue_description = await self.get_issue_details()
user_msg_template = jinja_env.get_template('jira_new_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.payload.issue_key,
issue_title=issue_title,
issue_description=issue_description,
user_message=self.payload.user_msg,
)
return user_msg
def _create_jira_v1_callback_processor(self):
"""Create a V1 callback processor for Jira integration."""
return JiraV1CallbackProcessor(
svc_acc_email=self.jira_workspace.svc_acc_email,
decrypted_api_key=self._decrypted_api_key,
issue_key=self.payload.issue_key,
jira_cloud_id=self.jira_workspace.jira_cloud_id,
)
async def _get_resolved_org_id(self) -> UUID | None:
"""Resolve the org ID for V1 conversations."""
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = await self._get_instructions(jinja_env)
if not provider_tokens:
return None
try:
agent_loop_info = await create_new_conversation(
user_id=self.jira_user.keycloak_user_id,
git_provider_tokens=provider_tokens,
selected_repository=self.selected_repo,
selected_branch=None,
initial_user_msg=user_msg,
conversation_instructions=instructions,
image_urls=None,
replay_json=None,
conversation_trigger=ConversationTrigger.JIRA,
custom_secrets=user_secrets.custom_secrets if user_secrets else None,
provider_handler = ProviderHandler(provider_tokens)
repository = await provider_handler.verify_repo_provider(self.selected_repo)
resolved_org_id = await resolve_org_for_repo(
provider=repository.git_provider.value,
full_repo_name=self.selected_repo,
keycloak_user_id=self.jira_user.keycloak_user_id,
)
self.conversation_id = agent_loop_info.conversation_id
logger.info(
'[Jira] Created conversation',
extra={
'conversation_id': self.conversation_id,
'issue_key': self.payload.issue_key,
'selected_repo': self.selected_repo,
},
)
# Store Jira conversation mapping
jira_conversation = JiraConversation(
conversation_id=self.conversation_id,
issue_id=self.payload.issue_id,
issue_key=self.payload.issue_key,
jira_user_id=self.jira_user.id,
)
await integration_store.create_conversation(jira_conversation)
return self.conversation_id
return resolved_org_id
except Exception as e:
if isinstance(e, StartingConvoException):
raise
logger.error(
'[Jira] Failed to create conversation',
extra={'issue_key': self.payload.issue_key, 'error': str(e)},
exc_info=True,
logger.warning(
f'[Jira] Failed to resolve org for {self.selected_repo}: {e}'
)
raise StartingConvoException(f'Failed to create conversation: {str(e)}')
return None
def get_response_msg(self) -> str:
"""Get the response message to send back to Jira."""

View File

@@ -20,11 +20,11 @@ from integrations.utils import (
OPENHANDS_RESOLVER_TEMPLATES_DIR,
filter_potential_repos_by_user_msg,
get_session_expired_message,
markdown_to_jira_markup,
)
from jinja2 import Environment, FileSystemLoader
from server.auth.saas_user_auth import get_user_auth_from_keycloak_id
from server.auth.token_manager import TokenManager
from server.utils.conversation_callback_utils import register_callback_processor
from storage.jira_dc_integration_store import JiraDcIntegrationStore
from storage.jira_dc_user import JiraDcUser
from storage.jira_dc_workspace import JiraDcWorkspace
@@ -42,7 +42,7 @@ from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.http_session import httpx_verify_option
class JiraDcManager(Manager):
class JiraDcManager(Manager[JiraDcViewInterface]):
def __init__(self, token_manager: TokenManager):
self.token_manager = token_manager
self.integration_store = JiraDcIntegrationStore.get_instance()
@@ -353,13 +353,8 @@ class JiraDcManager(Manager):
logger.error(f'[Jira DC] Error in is_job_requested: {str(e)}')
return False
async def start_job(self, jira_dc_view: JiraDcViewInterface):
"""Start a Jira DC job/conversation."""
# Import here to prevent circular import
from server.conversation_callback_processor.jira_dc_callback_processor import (
JiraDcCallbackProcessor,
)
async def start_job(self, jira_dc_view: JiraDcViewInterface) -> None:
"""Start a Jira DC job/conversation using V1 app conversation system."""
try:
user_info: JiraDcUser = jira_dc_view.jira_dc_user
logger.info(
@@ -367,7 +362,15 @@ class JiraDcManager(Manager):
f'issue {jira_dc_view.job_context.issue_key}',
)
# Create conversation
# Set decrypted API key for new conversations (needed for V1 callback processor)
if isinstance(jira_dc_view, JiraDcNewConversationView):
api_key = self.token_manager.decrypt_text(
jira_dc_view.jira_dc_workspace.svc_acc_api_key
)
jira_dc_view._decrypted_api_key = api_key
# Create conversation using V1 app conversation system
# The callback processor is registered automatically by the view
conversation_id = await jira_dc_view.create_or_update_conversation(
self.jinja_env
)
@@ -376,21 +379,6 @@ class JiraDcManager(Manager):
f'[Jira DC] Created/Updated conversation {conversation_id} for issue {jira_dc_view.job_context.issue_key}'
)
if isinstance(jira_dc_view, JiraDcNewConversationView):
# Register callback processor for updates
processor = JiraDcCallbackProcessor(
issue_key=jira_dc_view.job_context.issue_key,
workspace_name=jira_dc_view.jira_dc_workspace.name,
base_api_url=jira_dc_view.job_context.base_api_url,
)
# Register the callback processor
register_callback_processor(conversation_id, processor)
logger.info(
f'[Jira DC] Created callback processor for conversation {conversation_id}'
)
# Send initial response
msg_info = jira_dc_view.get_response_msg()
@@ -418,7 +406,7 @@ class JiraDcManager(Manager):
jira_dc_view.jira_dc_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(msg=msg_info),
msg_info,
issue_key=jira_dc_view.job_context.issue_key,
base_api_url=jira_dc_view.job_context.base_api_url,
svc_acc_api_key=api_key,
@@ -456,12 +444,20 @@ class JiraDcManager(Manager):
return title, description
async def send_message(
self, message: Message, issue_key: str, base_api_url: str, svc_acc_api_key: str
self, message: str, issue_key: str, base_api_url: str, svc_acc_api_key: str
):
"""Send message/comment to Jira DC issue."""
"""Send message/comment to Jira DC issue.
Args:
message: The message content to send (plain text string)
issue_key: The Jira issue key (e.g., 'PROJ-123')
base_api_url: The base API URL for the Jira DC instance
svc_acc_api_key: Service account API key for authentication
"""
url = f'{base_api_url}/rest/api/2/issue/{issue_key}/comment'
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
data = {'body': message.message}
# Convert standard Markdown to Jira Wiki Markup for proper rendering
data = {'body': markdown_to_jira_markup(message)}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(url, headers=headers, json=data)
response.raise_for_status()
@@ -481,7 +477,7 @@ class JiraDcManager(Manager):
try:
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
await self.send_message(
self.create_outgoing_message(msg=error_msg),
error_msg,
issue_key=job_context.issue_key,
base_api_url=job_context.base_api_url,
svc_acc_api_key=api_key,
@@ -502,7 +498,7 @@ class JiraDcManager(Manager):
)
await self.send_message(
self.create_outgoing_message(msg=comment_msg),
comment_msg,
issue_key=jira_dc_view.job_context.issue_key,
base_api_url=jira_dc_view.job_context.base_api_url,
svc_acc_api_key=api_key,

View File

@@ -19,7 +19,7 @@ class JiraDcViewInterface(ABC):
conversation_id: str
@abstractmethod
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Get initial instructions for the conversation."""
pass

View File

@@ -0,0 +1,243 @@
"""Jira Data Center V1 callback processor.
This processor handles events from V1 conversations and posts
summaries back to Jira DC issues when the agent finishes work.
"""
import logging
from uuid import UUID
import httpx
from integrations.utils import get_summary_instruction, markdown_to_jira_markup
from pydantic import Field
from openhands.agent_server.models import AskAgentRequest, AskAgentResponse
from openhands.app_server.event_callback.event_callback_models import (
EventCallback,
EventCallbackProcessor,
)
from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResult,
EventCallbackResultStatus,
)
from openhands.app_server.event_callback.util import (
ensure_conversation_found,
ensure_running_sandbox,
get_agent_server_url_from_sandbox,
)
from openhands.sdk import Event
from openhands.sdk.event import ConversationStateUpdateEvent
from openhands.utils.http_session import httpx_verify_option
_logger = logging.getLogger(__name__)
class JiraDcV1CallbackProcessor(EventCallbackProcessor):
"""Callback processor for Jira Data Center V1 integrations."""
should_request_summary: bool = Field(default=True)
issue_key: str
workspace_name: str
base_api_url: str
svc_acc_api_key: str # Decrypted API key
async def __call__(
self,
conversation_id: UUID,
callback: EventCallback,
event: Event,
) -> EventCallbackResult | None:
"""Process events for Jira DC V1 integration."""
# Only handle ConversationStateUpdateEvent for execution_status
if not isinstance(event, ConversationStateUpdateEvent):
return None
if event.key != 'execution_status':
return None
_logger.info('[Jira DC] Callback agent state was %s', event)
# Only request summary when execution has finished successfully
if event.value != 'finished':
return None
_logger.info(
'[Jira DC] Should request summary: %s', self.should_request_summary
)
if not self.should_request_summary:
return None
self.should_request_summary = False
try:
_logger.info(f'[Jira DC] Requesting summary {conversation_id}')
summary = await self._request_summary(conversation_id)
_logger.info(
f'[Jira DC] Posting summary {conversation_id}',
extra={'summary': summary},
)
await self._post_summary_to_jira_dc(summary)
return EventCallbackResult(
status=EventCallbackResultStatus.SUCCESS,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=summary,
)
except Exception as e:
_logger.exception(f'[Jira DC] Failed to post summary: {e}', stack_info=True)
return EventCallbackResult(
status=EventCallbackResultStatus.ERROR,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=str(e),
)
async def _request_summary(self, conversation_id: UUID) -> str:
"""Ask the agent to produce a summary of its work and return the agent response."""
# Import services within the method to avoid circular imports
from openhands.app_server.config import (
get_app_conversation_info_service,
get_httpx_client,
get_sandbox_service,
)
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import (
ADMIN,
USER_CONTEXT_ATTR,
)
# Create injector state for dependency injection
state = InjectorState()
setattr(state, USER_CONTEXT_ATTR, ADMIN)
async with (
get_app_conversation_info_service(state) as app_conversation_info_service,
get_sandbox_service(state) as sandbox_service,
get_httpx_client(state) as httpx_client,
):
# 1. Conversation lookup
app_conversation_info = ensure_conversation_found(
await app_conversation_info_service.get_app_conversation_info(
conversation_id
),
conversation_id,
)
# 2. Sandbox lookup + validation
sandbox = ensure_running_sandbox(
await sandbox_service.get_sandbox(app_conversation_info.sandbox_id),
app_conversation_info.sandbox_id,
)
assert (
sandbox.session_api_key is not None
), f'No session API key for sandbox: {sandbox.id}'
# 3. URL + instruction
agent_server_url = get_agent_server_url_from_sandbox(sandbox)
# Prepare message based on agent state
message_content = get_summary_instruction()
# Ask the agent and return the response text
return await self._ask_question(
httpx_client=httpx_client,
agent_server_url=agent_server_url,
conversation_id=conversation_id,
session_api_key=sandbox.session_api_key,
message_content=message_content,
)
async def _ask_question(
self,
httpx_client: httpx.AsyncClient,
agent_server_url: str,
conversation_id: UUID,
session_api_key: str,
message_content: str,
) -> str:
"""Send a message to the agent server via the V1 API and return response text."""
send_message_request = AskAgentRequest(question=message_content)
url = (
f"{agent_server_url.rstrip('/')}"
f"/api/conversations/{conversation_id}/ask_agent"
)
headers = {'X-Session-API-Key': session_api_key}
payload = send_message_request.model_dump()
try:
response = await httpx_client.post(
url,
json=payload,
headers=headers,
timeout=30.0,
)
response.raise_for_status()
agent_response = AskAgentResponse.model_validate(response.json())
return agent_response.response
except httpx.HTTPStatusError as e:
error_detail = f'HTTP {e.response.status_code} error'
try:
error_body = e.response.text
if error_body:
error_detail += f': {error_body}'
except Exception:
pass
_logger.exception(
'[Jira DC] HTTP error sending message to %s: %s. '
'Request payload: %s. Response headers: %s',
url,
error_detail,
payload,
dict(e.response.headers),
stack_info=True,
)
raise Exception(f'Failed to send message to agent server: {error_detail}')
except httpx.TimeoutException:
error_detail = f'Request timeout after 30 seconds to {url}'
_logger.exception(
'[Jira DC] Timeout error: %s. Request payload: %s',
error_detail,
payload,
stack_info=True,
)
raise Exception(f'Failed to send message to agent server: {error_detail}')
async def _post_summary_to_jira_dc(self, summary: str):
"""Post the summary back to the Jira DC issue."""
if not all(
[
self.svc_acc_api_key,
self.issue_key,
self.base_api_url,
]
):
_logger.warning('[Jira DC] Missing required data for posting summary')
return
# Add a comment to the Jira DC issue with the summary
comment_url = f'{self.base_api_url}/rest/api/2/issue/{self.issue_key}/comment'
message = f'OpenHands resolved this issue:\n\n{summary}'
# Convert standard Markdown to Jira Wiki Markup for proper rendering
comment_body = {'body': markdown_to_jira_markup(message)}
headers = {'Authorization': f'Bearer {self.svc_acc_api_key}'}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(
comment_url,
headers=headers,
json=comment_body,
)
response.raise_for_status()
_logger.info(f'[Jira DC] Posted summary to {self.issue_key}')

View File

@@ -1,34 +1,51 @@
from dataclasses import dataclass
"""Jira Data Center view implementations and factory.
Views are responsible for:
- Holding the webhook payload and auth context
- Creating conversations using V1 app conversation system
"""
from dataclasses import dataclass, field
from uuid import UUID, uuid4
from integrations.jira_dc.jira_dc_types import (
JiraDcViewInterface,
StartingConvoException,
)
from integrations.jira_dc.jira_dc_v1_callback_processor import JiraDcV1CallbackProcessor
from integrations.models import JobContext
from integrations.utils import CONVERSATION_URL, get_final_agent_observation
from integrations.resolver_context import ResolverUserContext
from integrations.resolver_org_router import resolve_org_for_repo
from integrations.utils import CONVERSATION_URL
from jinja2 import Environment
from storage.jira_dc_conversation import JiraDcConversation
from storage.jira_dc_integration_store import JiraDcIntegrationStore
from storage.jira_dc_user import JiraDcUser
from storage.jira_dc_workspace import JiraDcWorkspace
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema.agent import AgentState
from openhands.events.action import MessageAction
from openhands.events.serialization.event import event_to_dict
from openhands.server.services.conversation_service import (
create_new_conversation,
setup_init_conversation_settings,
from openhands.agent_server.models import SendMessageRequest
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartRequest,
AppConversationStartTaskStatus,
)
from openhands.server.shared import ConversationStoreImpl, config, conversation_manager
from openhands.app_server.config import get_app_conversation_service
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler, ProviderType
from openhands.sdk import TextContent
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
from openhands.storage.data_models.conversation_metadata import (
ConversationTrigger,
)
integration_store = JiraDcIntegrationStore.get_instance()
@dataclass
class JiraDcNewConversationView(JiraDcViewInterface):
"""View for creating a new Jira DC conversation."""
job_context: JobContext
saas_user_auth: UserAuth
jira_dc_user: JiraDcUser
@@ -36,9 +53,14 @@ class JiraDcNewConversationView(JiraDcViewInterface):
selected_repo: str | None
conversation_id: str
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
# Decrypted API key (set by manager)
_decrypted_api_key: str = field(default='', repr=False)
# Resolved org ID for V1 conversations
resolved_org_id: UUID | None = None
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized."""
instructions_template = jinja_env.get_template('jira_dc_instructions.j2')
instructions = instructions_template.render()
@@ -54,58 +76,148 @@ class JiraDcNewConversationView(JiraDcViewInterface):
return instructions, user_msg
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Create a new Jira DC conversation"""
"""Create a new Jira DC conversation using V1 app conversation system.
Returns:
The conversation ID
Raises:
StartingConvoException: If conversation creation fails
"""
if not self.selected_repo:
raise StartingConvoException('No repository selected for this conversation')
# Generate conversation ID
self.conversation_id = uuid4().hex
# Save the JiraDC conversation mapping
jira_dc_conversation = JiraDcConversation(
conversation_id=self.conversation_id,
issue_id=self.job_context.issue_id,
issue_key=self.job_context.issue_key,
jira_dc_user_id=self.jira_dc_user.id,
)
await integration_store.create_conversation(jira_dc_conversation)
# Create V1 conversation
await self._create_v1_conversation(jinja_env)
return self.conversation_id
async def _create_v1_conversation(self, jinja_env: Environment):
"""Create conversation using the V1 app conversation system."""
logger.info('[Jira DC]: Creating V1 conversation')
instructions, user_msg = await self._get_instructions(jinja_env)
# Create the initial message request
initial_message = SendMessageRequest(
role='user', content=[TextContent(text=user_msg)]
)
# Create the Jira DC V1 callback processor
jira_dc_callback_processor = self._create_jira_dc_v1_callback_processor()
# Resolve org ID for the V1 system
self.resolved_org_id = await self._get_resolved_org_id()
# Determine git provider
git_provider = await self._get_git_provider()
injector_state = InjectorState()
# Create the V1 conversation start request
start_request = AppConversationStartRequest(
conversation_id=UUID(self.conversation_id),
system_message_suffix=instructions if instructions else None,
initial_message=initial_message,
selected_repository=self.selected_repo,
selected_branch=None,
git_provider=git_provider,
title=f'Jira DC Issue {self.job_context.issue_key}: {self.job_context.issue_title or "Unknown"}',
trigger=ConversationTrigger.JIRA,
processors=[jira_dc_callback_processor],
)
# Set up the Jira DC user context for the V1 system
jira_dc_user_context = ResolverUserContext(
saas_user_auth=self.saas_user_auth,
resolver_org_id=self.resolved_org_id,
)
setattr(injector_state, USER_CONTEXT_ATTR, jira_dc_user_context)
async with get_app_conversation_service(
injector_state
) as app_conversation_service:
async for task in app_conversation_service.start_app_conversation(
start_request
):
if task.status == AppConversationStartTaskStatus.ERROR:
logger.error(f'Failed to start V1 conversation: {task.detail}')
raise RuntimeError(
f'Failed to start V1 conversation: {task.detail}'
)
logger.info(f'[Jira DC]: Created new conversation: {self.conversation_id}')
def _create_jira_dc_v1_callback_processor(self) -> JiraDcV1CallbackProcessor:
"""Create a V1 callback processor for Jira DC integration."""
return JiraDcV1CallbackProcessor(
issue_key=self.job_context.issue_key,
workspace_name=self.jira_dc_workspace.name,
base_api_url=self.job_context.base_api_url,
svc_acc_api_key=self._decrypted_api_key,
)
async def _get_git_provider(self) -> ProviderType | None:
"""Determine the git provider from the selected repository."""
if not self.selected_repo:
return None
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
if not provider_tokens:
return None
try:
agent_loop_info = await create_new_conversation(
user_id=self.jira_dc_user.keycloak_user_id,
git_provider_tokens=provider_tokens,
selected_repository=self.selected_repo,
selected_branch=None,
initial_user_msg=user_msg,
conversation_instructions=instructions,
image_urls=None,
replay_json=None,
conversation_trigger=ConversationTrigger.JIRA_DC,
custom_secrets=user_secrets.custom_secrets if user_secrets else None,
)
self.conversation_id = agent_loop_info.conversation_id
logger.info(f'[Jira DC] Created conversation {self.conversation_id}')
# Store Jira DC conversation mapping
jira_dc_conversation = JiraDcConversation(
conversation_id=self.conversation_id,
issue_id=self.job_context.issue_id,
issue_key=self.job_context.issue_key,
jira_dc_user_id=self.jira_dc_user.id,
)
await integration_store.create_conversation(jira_dc_conversation)
return self.conversation_id
provider_handler = ProviderHandler(provider_tokens)
repository = await provider_handler.verify_repo_provider(self.selected_repo)
return repository.git_provider
except Exception as e:
logger.error(
f'[Jira DC] Failed to create conversation: {str(e)}', exc_info=True
logger.warning(
f'[Jira DC] Failed to determine git provider for {self.selected_repo}: {e}'
)
raise StartingConvoException(f'Failed to create conversation: {str(e)}')
return None
async def _get_resolved_org_id(self) -> UUID | None:
"""Resolve the org ID for V1 conversations."""
provider_tokens = await self.saas_user_auth.get_provider_tokens()
if not provider_tokens or not self.selected_repo:
return None
try:
provider_handler = ProviderHandler(provider_tokens)
repository = await provider_handler.verify_repo_provider(self.selected_repo)
resolved_org_id = await resolve_org_for_repo(
provider=repository.git_provider.value,
full_repo_name=self.selected_repo,
keycloak_user_id=self.jira_dc_user.keycloak_user_id,
)
return resolved_org_id
except Exception as e:
logger.warning(
f'[Jira DC] Failed to resolve org for {self.selected_repo}: {e}'
)
return None
def get_response_msg(self) -> str:
"""Get the response message to send back to Jira DC"""
"""Get the response message to send back to Jira DC."""
conversation_link = CONVERSATION_URL.format(self.conversation_id)
return f"I'm on it! {self.job_context.display_name} can [track my progress here|{conversation_link}]."
@dataclass
class JiraDcExistingConversationView(JiraDcViewInterface):
"""View for sending messages to an existing Jira DC conversation."""
job_context: JobContext
saas_user_auth: UserAuth
jira_dc_user: JiraDcUser
@@ -113,9 +225,8 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
selected_repo: str | None
conversation_id: str
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is updated."""
user_msg_template = jinja_env.get_template('jira_dc_existing_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.job_context.issue_key,
@@ -127,61 +238,107 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
return '', user_msg
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Update an existing Jira conversation"""
"""Send a message to an existing V1 conversation.
user_id = self.jira_dc_user.keycloak_user_id
Returns:
The conversation ID
"""
await self._send_message_to_v1_conversation(jinja_env)
return self.conversation_id
try:
conversation_store = await ConversationStoreImpl.get_instance(
config, user_id
async def _send_message_to_v1_conversation(self, jinja_env: Environment):
"""Send a message to an existing V1 conversation using the agent server API."""
import httpx
from openhands.app_server.config import (
get_app_conversation_info_service,
get_httpx_client,
get_sandbox_service,
)
from openhands.app_server.event_callback.util import (
ensure_conversation_found,
get_agent_server_url_from_sandbox,
)
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.user.specifiy_user_context import (
ADMIN,
USER_CONTEXT_ATTR,
)
_, user_msg = await self._get_instructions(jinja_env)
# Create injector state for dependency injection
state = InjectorState()
setattr(state, USER_CONTEXT_ATTR, ADMIN)
async with (
get_app_conversation_info_service(state) as app_conversation_info_service,
get_sandbox_service(state) as sandbox_service,
get_httpx_client(state) as httpx_client,
):
# 1. Conversation lookup
conversation_uuid = UUID(self.conversation_id)
app_conversation_info = ensure_conversation_found(
await app_conversation_info_service.get_app_conversation_info(
conversation_uuid
),
conversation_uuid,
)
# 2. Sandbox lookup + validation
sandbox = await sandbox_service.get_sandbox(
app_conversation_info.sandbox_id
)
if sandbox is None or sandbox.status != SandboxStatus.RUNNING:
logger.warning(
f'[Jira DC] Sandbox not running for conversation {self.conversation_id}'
)
return
if sandbox.session_api_key is None:
logger.warning(
f'[Jira DC] No session API key for sandbox: {sandbox.id}'
)
return
# 3. Build URL and send message
agent_server_url = get_agent_server_url_from_sandbox(sandbox)
send_message_request = SendMessageRequest(
role='user', content=[TextContent(text=user_msg)]
)
url = (
f"{agent_server_url.rstrip('/')}"
f'/api/conversations/{self.conversation_id}/messages'
)
headers = {'X-Session-API-Key': sandbox.session_api_key}
payload = send_message_request.model_dump()
try:
await conversation_store.get_metadata(self.conversation_id)
except FileNotFoundError:
raise StartingConvoException('Conversation no longer exists.')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
if provider_tokens is None:
raise ValueError('Could not load provider tokens')
providers_set = list(provider_tokens.keys())
conversation_init_data = await setup_init_conversation_settings(
user_id, self.conversation_id, providers_set
)
# Either join ongoing conversation, or restart the conversation
agent_loop_info = await conversation_manager.maybe_start_agent_loop(
self.conversation_id, conversation_init_data, user_id
)
final_agent_observation = get_final_agent_observation(
agent_loop_info.event_store
)
agent_state = (
None
if len(final_agent_observation) == 0
else final_agent_observation[0].agent_state
)
if not agent_state or agent_state == AgentState.LOADING:
raise StartingConvoException('Conversation is still starting')
_, user_msg = self._get_instructions(jinja_env)
user_message_event = MessageAction(content=user_msg)
await conversation_manager.send_event_to_conversation(
self.conversation_id, event_to_dict(user_message_event)
)
return self.conversation_id
except Exception as e:
logger.error(
f'[Jira] Failed to create conversation: {str(e)}', exc_info=True
)
raise StartingConvoException(f'Failed to create conversation: {str(e)}')
response = await httpx_client.post(
url,
json=payload,
headers=headers,
timeout=30.0,
)
response.raise_for_status()
logger.info(
f'[Jira DC] Sent message to existing conversation {self.conversation_id}'
)
except httpx.HTTPStatusError as e:
logger.error(
f'[Jira DC] Failed to send message: HTTP {e.response.status_code}'
)
raise
except Exception as e:
logger.error(f'[Jira DC] Failed to send message: {e}')
raise
def get_response_msg(self) -> str:
"""Get the response message to send back to Jira"""
"""Get the response message to send back to Jira."""
conversation_link = CONVERSATION_URL.format(self.conversation_id)
return f"I'm on it! {self.job_context.display_name} can [continue tracking my progress here|{conversation_link}]."
@@ -197,7 +354,6 @@ class JiraDcFactory:
jira_dc_workspace: JiraDcWorkspace,
) -> JiraDcViewInterface:
"""Create appropriate Jira DC view based on the payload."""
if not jira_dc_user or not saas_user_auth or not jira_dc_workspace:
raise StartingConvoException('User not authenticated with Jira integration')

View File

@@ -1,532 +0,0 @@
import hashlib
import hmac
from typing import Dict, Optional, Tuple
import httpx
from fastapi import Request
from integrations.linear.linear_types import LinearViewInterface
from integrations.linear.linear_view import (
LinearExistingConversationView,
LinearFactory,
LinearNewConversationView,
)
from integrations.manager import Manager
from integrations.models import JobContext, Message
from integrations.utils import (
HOST_URL,
OPENHANDS_RESOLVER_TEMPLATES_DIR,
filter_potential_repos_by_user_msg,
get_session_expired_message,
)
from jinja2 import Environment, FileSystemLoader
from server.auth.saas_user_auth import get_user_auth_from_keycloak_id
from server.auth.token_manager import TokenManager
from server.utils.conversation_callback_utils import register_callback_processor
from storage.linear_integration_store import LinearIntegrationStore
from storage.linear_user import LinearUser
from storage.linear_workspace import LinearWorkspace
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import Repository
from openhands.server.shared import server_config
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.server.user_auth.user_auth import UserAuth
from openhands.utils.http_session import httpx_verify_option
class LinearManager(Manager):
def __init__(self, token_manager: TokenManager):
self.token_manager = token_manager
self.integration_store = LinearIntegrationStore.get_instance()
self.api_url = 'https://api.linear.app/graphql'
self.jinja_env = Environment(
loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR + 'linear')
)
async def authenticate_user(
self, linear_user_id: str, workspace_id: int
) -> tuple[LinearUser | None, UserAuth | None]:
"""Authenticate Linear user and get their OpenHands user auth."""
# Find active Linear user by Linear user ID and workspace ID
linear_user = await self.integration_store.get_active_user(
linear_user_id, workspace_id
)
if not linear_user:
logger.warning(
f'[Linear] No active Linear user found for {linear_user_id} in workspace {workspace_id}'
)
return None, None
saas_user_auth = await get_user_auth_from_keycloak_id(
linear_user.keycloak_user_id
)
return linear_user, saas_user_auth
async def _get_repositories(self, user_auth: UserAuth) -> list[Repository]:
"""Get repositories that the user has access to."""
provider_tokens = await user_auth.get_provider_tokens()
if provider_tokens is None:
return []
access_token = await user_auth.get_access_token()
user_id = await user_auth.get_user_id()
client = ProviderHandler(
provider_tokens=provider_tokens,
external_auth_token=access_token,
external_auth_id=user_id,
)
repos: list[Repository] = await client.get_repositories(
'pushed', server_config.app_mode, None, None, None, None
)
return repos
async def validate_request(
self, request: Request
) -> Tuple[bool, Optional[str], Optional[Dict]]:
"""Verify Linear webhook signature."""
signature = request.headers.get('linear-signature')
body = await request.body()
payload = await request.json()
actor_url = payload.get('actor', {}).get('url', '')
workspace_name = ''
# Extract workspace name from actor URL
# Format: https://linear.app/{workspace}/profiles/{user}
if actor_url.startswith('https://linear.app/'):
url_parts = actor_url.split('/')
if len(url_parts) >= 4:
workspace_name = url_parts[3] # Extract workspace name
else:
logger.warning(f'[Linear] Invalid actor URL format: {actor_url}')
return False, None, None
else:
logger.warning(
f'[Linear] Actor URL does not match expected format: {actor_url}'
)
return False, None, None
if not workspace_name:
logger.warning('[Linear] No workspace name found in webhook payload')
return False, None, None
if not signature:
logger.warning('[Linear] No signature found in webhook headers')
return False, None, None
workspace = await self.integration_store.get_workspace_by_name(workspace_name)
if not workspace:
logger.warning('[Linear] Could not identify workspace for webhook')
return False, None, None
if workspace.status != 'active':
logger.warning(f'[Linear] Workspace {workspace.id} is not active')
return False, None, None
webhook_secret = self.token_manager.decrypt_text(workspace.webhook_secret)
digest = hmac.new(webhook_secret.encode(), body, hashlib.sha256).hexdigest()
if hmac.compare_digest(signature, digest):
logger.info('[Linear] Webhook signature verified successfully')
return True, signature, payload
return False, None, None
def parse_webhook(self, payload: Dict) -> JobContext | None:
action = payload.get('action')
type = payload.get('type')
if action == 'create' and type == 'Comment':
data = payload.get('data', {})
comment = data.get('body', '')
if '@openhands' not in comment:
return None
issue_data = data.get('issue', {})
issue_id = issue_data.get('id', '')
issue_key = issue_data.get('identifier', '')
elif action == 'update' and type == 'Issue':
data = payload.get('data', {})
labels = data.get('labels', [])
has_openhands_label = False
label_id = ''
for label in labels:
if label.get('name') == 'openhands':
label_id = label.get('id', '')
has_openhands_label = True
break
if not has_openhands_label and not label_id:
return None
labelIdChanges = data.get('updatedFrom', {}).get('labelIds', [])
if labelIdChanges and label_id in labelIdChanges:
return None # Label was added previously, ignore this webhook
issue_id = data.get('id', '')
issue_key = data.get('identifier', '')
comment = ''
else:
return None
actor = payload.get('actor', {})
display_name = actor.get('name', '')
user_email = actor.get('email', '')
actor_url = actor.get('url', '')
actor_id = actor.get('id', '')
workspace_name = ''
if actor_url.startswith('https://linear.app/'):
url_parts = actor_url.split('/')
if len(url_parts) >= 4:
workspace_name = url_parts[3] # Extract workspace name
else:
logger.warning(f'[Linear] Invalid actor URL format: {actor_url}')
return None
else:
logger.warning(
f'[Linear] Actor URL does not match expected format: {actor_url}'
)
return None
if not all(
[issue_id, issue_key, display_name, user_email, actor_id, workspace_name]
):
logger.warning('[Linear] Missing required fields in webhook payload')
return None
return JobContext(
issue_id=issue_id,
issue_key=issue_key,
user_msg=comment,
user_email=user_email,
platform_user_id=actor_id,
workspace_name=workspace_name,
display_name=display_name,
)
async def receive_message(self, message: Message):
"""Process incoming Linear webhook message."""
payload = message.message.get('payload', {})
job_context = self.parse_webhook(payload)
if not job_context:
logger.info('[Linear] Webhook does not match trigger conditions')
return
# Get workspace by user email domain
workspace = await self.integration_store.get_workspace_by_name(
job_context.workspace_name
)
if not workspace:
logger.warning(
f'[Linear] No workspace found for email domain: {job_context.workspace_name}'
)
await self._send_error_comment(
job_context.issue_id,
'Your workspace is not configured with Linear integration.',
None,
)
return
# Prevent any recursive triggers from the service account
if job_context.user_email == workspace.svc_acc_email:
return
if workspace.status != 'active':
logger.warning(f'[Linear] Workspace {workspace.id} is not active')
await self._send_error_comment(
job_context.issue_id,
'Linear integration is not active for your workspace.',
workspace,
)
return
# Authenticate user
linear_user, saas_user_auth = await self.authenticate_user(
job_context.platform_user_id, workspace.id
)
if not linear_user or not saas_user_auth:
logger.warning(
f'[Linear] User authentication failed for {job_context.user_email}'
)
await self._send_error_comment(
job_context.issue_id,
f'User {job_context.user_email} is not authenticated or active in the Linear integration.',
workspace,
)
return
# Get issue details
try:
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
issue_title, issue_description = await self.get_issue_details(
job_context.issue_id, api_key
)
job_context.issue_title = issue_title
job_context.issue_description = issue_description
except Exception as e:
logger.error(f'[Linear] Failed to get issue context: {str(e)}')
await self._send_error_comment(
job_context.issue_id,
'Failed to retrieve issue details. Please check the issue ID and try again.',
workspace,
)
return
try:
# Create Linear view
linear_view = await LinearFactory.create_linear_view_from_payload(
job_context,
saas_user_auth,
linear_user,
workspace,
)
except Exception as e:
logger.error(
f'[Linear] Failed to create linear view: {str(e)}', exc_info=True
)
await self._send_error_comment(
job_context.issue_id,
'Failed to initialize conversation. Please try again.',
workspace,
)
return
if not await self.is_job_requested(message, linear_view):
return
await self.start_job(linear_view)
async def is_job_requested(
self, message: Message, linear_view: LinearViewInterface
) -> bool:
"""
Check if a job is requested and handle repository selection.
"""
if isinstance(linear_view, LinearExistingConversationView):
return True
try:
# Get user repositories
user_repos: list[Repository] = await self._get_repositories(
linear_view.saas_user_auth
)
target_str = f'{linear_view.job_context.issue_description}\n{linear_view.job_context.user_msg}'
# Try to infer repository from issue description
match, repos = filter_potential_repos_by_user_msg(target_str, user_repos)
if match:
# Found exact repository match
linear_view.selected_repo = repos[0].full_name
logger.info(f'[Linear] Inferred repository: {repos[0].full_name}')
return True
else:
# No clear match - send repository selection comment
await self._send_repo_selection_comment(linear_view)
return False
except Exception as e:
logger.error(f'[Linear] Error in is_job_requested: {str(e)}')
return False
async def start_job(self, linear_view: LinearViewInterface):
"""Start a Linear job/conversation."""
# Import here to prevent circular import
from server.conversation_callback_processor.linear_callback_processor import (
LinearCallbackProcessor,
)
try:
user_info: LinearUser = linear_view.linear_user
logger.info(
f'[Linear] Starting job for user {user_info.keycloak_user_id} '
f'issue {linear_view.job_context.issue_key}',
)
# Create conversation
conversation_id = await linear_view.create_or_update_conversation(
self.jinja_env
)
logger.info(
f'[Linear] Created/Updated conversation {conversation_id} for issue {linear_view.job_context.issue_key}'
)
if isinstance(linear_view, LinearNewConversationView):
# Register callback processor for updates
processor = LinearCallbackProcessor(
issue_id=linear_view.job_context.issue_id,
issue_key=linear_view.job_context.issue_key,
workspace_name=linear_view.linear_workspace.name,
)
# Register the callback processor
register_callback_processor(conversation_id, processor)
logger.info(
f'[Linear] Created callback processor for conversation {conversation_id}'
)
# Send initial response
msg_info = linear_view.get_response_msg()
except MissingSettingsError as e:
logger.warning(f'[Linear] Missing settings error: {str(e)}')
msg_info = f'Please re-login into [OpenHands Cloud]({HOST_URL}) before starting a job.'
except LLMAuthenticationError as e:
logger.warning(f'[Linear] LLM authentication error: {str(e)}')
msg_info = f'Please set a valid LLM API key in [OpenHands Cloud]({HOST_URL}) before starting a job.'
except SessionExpiredError as e:
logger.warning(f'[Linear] Session expired: {str(e)}')
msg_info = get_session_expired_message()
except Exception as e:
logger.error(
f'[Linear] Unexpected error starting job: {str(e)}', exc_info=True
)
msg_info = 'Sorry, there was an unexpected error starting the job. Please try again.'
# Send response comment
try:
api_key = self.token_manager.decrypt_text(
linear_view.linear_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(msg=msg_info),
linear_view.job_context.issue_id,
api_key,
)
except Exception as e:
logger.error(f'[Linear] Failed to send response message: {str(e)}')
async def _query_api(self, query: str, variables: Dict, api_key: str) -> Dict:
"""Query Linear GraphQL API."""
headers = {'Authorization': api_key}
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(
self.api_url,
headers=headers,
json={'query': query, 'variables': variables},
)
response.raise_for_status()
return response.json()
async def get_issue_details(self, issue_id: str, api_key: str) -> Tuple[str, str]:
"""Get issue details from Linear API."""
query = """
query Issue($issueId: String!) {
issue(id: $issueId) {
id
identifier
title
description
syncedWith {
metadata {
... on ExternalEntityInfoGithubMetadata {
owner
repo
}
}
}
}
}
"""
issue_payload = await self._query_api(query, {'issueId': issue_id}, api_key)
if not issue_payload:
raise ValueError(f'Issue with ID {issue_id} not found.')
issue_data = issue_payload.get('data', {}).get('issue', {})
title = issue_data.get('title', '')
description = issue_data.get('description', '')
synced_with = issue_data.get('syncedWith', [])
owner = ''
repo = ''
if synced_with:
owner = synced_with[0].get('metadata', {}).get('owner', '')
repo = synced_with[0].get('metadata', {}).get('repo', '')
if not title:
raise ValueError(f'Issue with ID {issue_id} does not have a title.')
if not description:
raise ValueError(f'Issue with ID {issue_id} does not have a description.')
if owner and repo:
description += f'\n\nGit Repo: {owner}/{repo}'
return title, description
async def send_message(self, message: Message, issue_id: str, api_key: str):
"""Send message/comment to Linear issue."""
query = """
mutation CommentCreate($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
comment {
id
}
}
}
"""
variables = {'input': {'issueId': issue_id, 'body': message.message}}
return await self._query_api(query, variables, api_key)
async def _send_error_comment(
self, issue_id: str, error_msg: str, workspace: LinearWorkspace | None
):
"""Send error comment to Linear issue."""
if not workspace:
logger.error('[Linear] Cannot send error comment - no workspace available')
return
try:
api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
await self.send_message(
self.create_outgoing_message(msg=error_msg), issue_id, api_key
)
except Exception as e:
logger.error(f'[Linear] Failed to send error comment: {str(e)}')
async def _send_repo_selection_comment(self, linear_view: LinearViewInterface):
"""Send a comment with repository options for the user to choose."""
try:
comment_msg = (
'I need to know which repository to work with. '
'Please add it to your issue description or send a followup comment.'
)
api_key = self.token_manager.decrypt_text(
linear_view.linear_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(msg=comment_msg),
linear_view.job_context.issue_id,
api_key,
)
logger.info(
f'[Linear] Sent repository selection comment for issue {linear_view.job_context.issue_key}'
)
except Exception as e:
logger.error(
f'[Linear] Failed to send repository selection comment: {str(e)}'
)

View File

@@ -1,40 +0,0 @@
from abc import ABC, abstractmethod
from integrations.models import JobContext
from jinja2 import Environment
from storage.linear_user import LinearUser
from storage.linear_workspace import LinearWorkspace
from openhands.server.user_auth.user_auth import UserAuth
class LinearViewInterface(ABC):
"""Interface for Linear views that handle different types of Linear interactions."""
job_context: JobContext
saas_user_auth: UserAuth
linear_user: LinearUser
linear_workspace: LinearWorkspace
selected_repo: str | None
conversation_id: str
@abstractmethod
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Get initial instructions for the conversation."""
pass
@abstractmethod
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Create or update a conversation and return the conversation ID."""
pass
@abstractmethod
def get_response_msg(self) -> str:
"""Get the response message to send back to Linear."""
pass
class StartingConvoException(Exception):
"""Exception raised when starting a conversation fails."""
pass

Some files were not shown because too many files have changed in this diff Show More