Compare commits

..

661 Commits

Author SHA1 Message Date
chuckbutkus
226d1f8a83 Merge branch 'main' into fix-basic-settings 2026-02-05 15:25:41 -05:00
Chuck Butkus
3a9ef1a538 Change key gen for only when necessary 2026-02-05 15:05:47 -05: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
Chuck Butkus
b607bc20ca Generate key if necessary 2026-02-04 19:09:23 -05: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
Tim O'Farrell
102095affb Add downgrade script and methods for reverting user migration (#12629)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@all-hands.dev>
2026-01-27 14:41:34 -07:00
John-Mason P. Shackelford
b6ce45b474 feat(app_server): start conversations with remote plugins via REST API (#12338)
- Add `PluginSpec` model with plugin configuration parameters extending SDK's `PluginSource`
- Extend app-conversations API to accept plugins specification in `AppConversationStartRequest`
- Propagate plugin source, ref, and repo_path to agent server's `StartConversationRequest`
- Include plugin parameters in initial conversation message for agent context

Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-27 16:26:38 -05:00
Tim O'Farrell
11c87caba4 fix(backend): fix callback state not persisting due to dual-session conflict (#12627)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-27 10:02:48 -07:00
Abhay Mishra
b8a608c45e Fix: branch/repo dropdown reset on click (#12501)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-01-27 16:08:08 +07:00
Tim O'Farrell
8a446787be Migrate SDK to 1.10.0 (#12614) 2026-01-27 04:26:06 +00:00
Tim O'Farrell
353124e171 Bump SDK to 1.10.0 (#12613) 2026-01-27 03:50:30 +00:00
chuckbutkus
e9298c89bd Fix org migration (#12612)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-26 20:55:55 -05:00
Hiep Le
29b77be807 fix: revert get_user_by_id_async to use sync session_maker (#12610) 2026-01-27 04:39:07 +07:00
mamoodi
7094835ef0 Fix Pydantic UnsupportedFieldAttributeWarning in Settings model (#12600)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-26 12:30:43 -05:00
mamoodi
7ad0eec325 Change error to warning if TOS not accepted (#12605) 2026-01-26 12:30:00 -05:00
Hiep Le
31d5081163 feat(frontend): display plan preview content (#12504) 2026-01-26 23:19:57 +07:00
Abhay Mishra
250736cb7a fix(frontend): display ThinkAction thought content in V1 UI (#12597)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-01-26 14:03:16 +07:00
Tim O'Farrell
a9bd3a70c9 Fix V0 Integrations (#12584) 2026-01-24 16:53:16 +00:00
Hiep Le
d7436a4af4 fix(backend): asyncsession query error in userstore.get_user_by_id_async (#12586)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-24 23:47:04 +07:00
Tim O'Farrell
f327e76be7 Added explicit expired error (#12580) 2026-01-23 12:49:10 -07:00
Hiep Le
52e39e5d12 fix(backend): unable to export conversation (#12577)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-24 01:06:02 +07:00
Graham Neubig
6c5ef256fd fix: pass userId to EmailVerificationModal in login page (#12573)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-01-23 23:24:25 +07:00
Tim O'Farrell
373ade8554 Fix org billing (#12562) 2026-01-23 08:42:50 -07:00
Hiep Le
9d0a19cf8f fix(backend): ensure conversation events are written back to google cloud (#12571) 2026-01-23 22:13:08 +07:00
Rohit Malhotra
d60dd38d78 fix: preserve query params in returnTo during login redirect (#12567)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-22 21:31:42 -08:00
Hiep Le
d5ee799670 feat(backend): develop patch /api/organizations/{orgid} api (#12470)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2026-01-23 01:29:35 +07:00
Hiep Le
b685fd43dd fix(backend): github proxy state compression (#12387)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2026-01-23 00:39:03 +07:00
Hiep Le
0e04f6fdbe feat(backend): develop delete /api/organizations/{orgid} api (#12471)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2026-01-23 00:28:55 +07:00
Hiep Le
9c40929197 feat(backend): develop get /api/organizations/{orgid} api (#12274)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-01-22 23:55:29 +07:00
Hiep Le
af309e8586 feat(backend): develop get /api/organizations api (#12373)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2026-01-22 23:32:42 +07:00
sp.wack
cc5d5c2335 test(frontend): add missing HTTP mocks for conversation history preloading (#12549) 2026-01-22 20:30:27 +04:00
Hiep Le
60e668f4a7 fix(backend): application settings are not updating as expected (#12547) 2026-01-22 21:54:52 +07:00
Hiep Le
743f6256a6 feat(backend): load skills from agent server (#12434) 2026-01-22 20:20:50 +07:00
Mohammed Abdulai
a87b4efd41 feat: preload conversation history before websocket connection (#12488)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2026-01-22 12:41:27 +00:00
Tim O'Farrell
730d9970f5 Migrate SDK to 1.9.1 (#12540) 2026-01-21 16:14:27 -07:00
Tim O'Farrell
2440593431 Simplify app conversation (#12517) 2026-01-21 13:03:28 -07:00
Tim O'Farrell
8c94ddbf1a Bump the SDK to 1.9.0 (#12535) 2026-01-21 13:02:45 -07:00
Tim O'Farrell
af60183f22 Optimize runtime build (#12531) 2026-01-21 19:35:11 +00:00
Timon
40fb69325f fix: use correct agent-server image in docker-compose.yml (#12527) 2026-01-21 12:03:01 -07:00
Rohit Malhotra
f9891a2c0d [Jira]: improve traceability and reliability fixes (#12515)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-21 13:50:41 -05:00
Hiep Le
44f1bb022b fix(frontend): sub-conversation ids are missing when navigating from the conversation panel (#12506) 2026-01-22 00:27:48 +07:00
dependabot[bot]
95c2a0285c chore(deps): bump the version-all group in /frontend with 7 updates (#12524)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 20:47:09 +04:00
Tim O'Farrell
8ea4813936 fix: remove keycloak_auth cookie requirement for webhook secrets endpoint (#12525)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-21 09:17:19 -07:00
Graham Neubig
650bf8c0c0 docs: Update docker-compose files to use V1 environment variables (#12522)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-21 10:22:50 -05:00
Mohammed Abdulai
dd4bb5d362 Improve Browser and App tab empty state guidance (#12418) 2026-01-21 21:09:38 +07:00
Graham Neubig
b3137e7ae8 fix: remove frozen=True from Field() to fix Pydantic warning (#12518)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-21 08:50:49 -05:00
dependabot[bot]
0d740925c5 chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.91.2 to 5.91.3 in /frontend in the eslint group (#12497)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 16:34:44 +04:00
Hiep Le
030ff59c40 feat(backend): develop post /api/organizations api (org project) (#12263)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2026-01-21 15:14:27 +07:00
Rohit Malhotra
3bc56740b9 [Jira]: separate signature verification from conversation orchestration (#12478)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-20 16:21:23 -05:00
Hiep Le
87a5762bf2 fix: handle invalid LiteLLM API keys during user migration (#12510) 2026-01-20 13:11:49 -07:00
Rohit Malhotra
5b149927fb [Jira]: fix match conditions for kicking off openhands (#12477)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-20 15:10:03 -05:00
Hiep Le
275bb689e4 fix(backend): sub-conversations can no longer be linked to parent conversations due to recent v1 changes (#12505) 2026-01-20 23:59:30 +07:00
Hiep Le
d595945309 fix(backend): handle existing LiteLLM users and team members during sign-in (#12508) 2026-01-20 23:35:37 +07:00
Hiep Le
e8ba741f28 fix(backend): byor key alias cleanup on 404 (#12491) 2026-01-19 13:12:04 +07:00
Graham Neubig
57929b73ee fix(frontend): show friendly message for budget/credit errors in V1 conversations (#12401)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-19 00:52:44 +00:00
Tim O'Farrell
9fd4e42438 Add CORS origins support to Docker sandbox service for remote browser access (#12489)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-18 08:02:29 -07:00
Tim O'Farrell
0176cd9669 Add LLM model to shared conversation view and remove feature flag (#12476)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-16 21:27:20 -07:00
Rohit Malhotra
2e9b29970e Extend expiration time for CLI device links (#12480) 2026-01-16 19:43:05 -05:00
chuckbutkus
a07fc1510b Add variable to disable duplicate email check (#12474) 2026-01-16 18:04:15 -05:00
dependabot[bot]
78f067acef chore(deps): bump the version-all group in /frontend with 5 updates (#12437)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 01:38:24 +04:00
Tim O'Farrell
0d5f97c8c7 Refactor enterprise database.py to use DbSessionInjector (#12446)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-16 14:38:05 -07:00
chuckbutkus
a987387353 Fix asyncio event loop conflict in get_user_by_id (#12475)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
2026-01-16 20:37:04 +00:00
Rohit Malhotra
941b6dd340 Jira: don't lock conversation to ticket (#12472)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-16 14:12:27 -05:00
Hiep Le
23251a2487 fix: cannot run code locally in SaaS mode (#12465) 2026-01-16 08:42:19 -07:00
Tim O'Farrell
6af8301996 Default value for V1 enabled should be true (#12469) 2026-01-16 15:39:22 +00:00
mamoodi
b57ec9eda1 Release 1.2.0 and 1.2.1 (#12440) 2026-01-16 09:33:41 -05:00
Tim O'Farrell
c8594a2eaa (Backend) New Config Endpoint (#12460) 2026-01-15 22:06:39 -07:00
Rohit Malhotra
102715a3c9 (Hotfix): missing provider tokens in v1 resolvers (#12453)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-15 22:22:10 -05:00
chuckbutkus
d5e66b4f3a SAAS: Introducing orgs (phase 1) (#11265)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-01-15 22:03:31 -05:00
Graham Neubig
f5315887ec Fix: Reduce log noise for optional org-level .openhands repositories (#12456)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-15 23:27:34 +00:00
Tim O'Farrell
6e4ac8e2ce [APP-373] Update legacy v0 deprecation notice with version and removal date (#12455)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-15 15:22:38 -07:00
Vedant Madane
efb54fd791 fix(frontend): dismissible & expandable error banner (#12354)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-15 21:18:37 +00:00
sp.wack
53f86955e0 fix(frontend): clear error banner on successful V1 WebSocket events (#12398) 2026-01-16 01:12:55 +04:00
MkDev11
b5a615aa67 fix: preserve HTTP protocol for self-hosted Gitea/Forgejo instances (#12439) 2026-01-15 12:20:07 -05:00
Mohammed Abdulai
3cb76f5cbb fix: clean up conversation localStorage on delete and migration (#12300)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-15 15:28:24 +00:00
Tim O'Farrell
91fb97a13f Migrate the SDK to 1.8.2 (#12438) 2026-01-15 15:18:26 +00:00
Graham Neubig
9af3ee8298 fix: Add WORKER_1 and WORKER_2 env vars to remote sandbox service (#12424)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-15 08:53:04 -05:00
Saurya Velagapudi
169ca5aae9 UV Migration Steps 1.3-1.6: Add project dependencies and generate uv.lock (#12416)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Saurya <saurya@openhands.dev>
2026-01-14 19:32:31 -07:00
Pegasus
3c6edfe14b fix(frontend): Respect HIDE_LLM_SETTINGS flag in settings modal (#12400) 2026-01-14 17:14:32 -06:00
dependabot[bot]
633552a731 chore(deps-dev): bump eslint-plugin-prettier from 5.5.4 to 5.5.5 in /frontend in the eslint group (#12408)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-15 01:09:36 +04:00
Saurya Velagapudi
3da24da4a0 UV Migration Step 1.1: Add PEP 621 [project] section for UV compatibility (#12414)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-14 20:59:15 +00:00
Tim O'Farrell
f28ab56cc3 fix: require reCAPTCHA token when reCAPTCHA is enabled (#12409)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-01-14 12:34:09 -07:00
Tim O'Farrell
6ccd42bb29 [APP-369] Fix: Allow public access to shared conversations and events (#12411)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-14 16:47:47 +00:00
Hiep Le
1146ea2274 refactor(frontend): create a feature flag for google recaptcha (#12402) 2026-01-14 19:22:38 +07:00
sp.wack
ff28e13698 hotfix(frontend): Fix auth refetch loading spinner flash (#12396) 2026-01-13 19:08:38 +00:00
Xingyao Wang
9171986dde Consolidate repo guidance into AGENTS.md (#12375)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-13 15:53:14 +00:00
Jatoth Adithya Naik
9d405243b8 Fix tab pin/unpin by aligning localStorage key per conversation- #12287 (#12292)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-13 15:25:08 +00:00
dependabot[bot]
d7218925c4 chore(deps): bump the version-all group in /frontend with 3 updates (#12390)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 19:10:17 +04:00
sp.wack
27c16d6691 chore(frontend): Cross-domain PostHog tracking (#12166) 2026-01-13 18:07:56 +04:00
dependabot[bot]
eabba5c160 chore(deps): bump the version-all group in /frontend with 10 updates (#12379)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 17:54:20 +04:00
Hiep Le
ece7e2dd39 refactor(frontend): update tooltip styling (#12371) 2026-01-13 11:59:40 +07:00
Tim O'Farrell
13762eba7c Add optional sandbox_id parameter to start_sandbox method (#12382)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-12 15:43:24 -07:00
Tim O'Farrell
9cf7d64bfe Guard User Creation with Redis based Lock (#12381)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-12 22:03:42 +00:00
Xingyao Wang
92baebc4bd Remove prod/ prefix from litellm proxy model path (#12200)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-13 04:46:09 +08:00
Hiep Le
3d0aa50450 refactor(frontend): make memory condenser max history size input styling consistent (#12365) 2026-01-12 21:43:06 +07:00
Hiep Le
0e3332d974 fix(frontend): make secrets table responsive when descriptions are long (#12363) 2026-01-12 21:40:09 +07:00
Saurya Velagapudi
3c6d2ff1d6 Add conversation API deprecation notices (#12303)
Co-authored-by: Saurya <saurya@openhands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-01-12 08:26:33 -05:00
Hiep Le
b7b76c7a30 refactor: update the pull request template to add optional demo screenshots and videos (#12367) 2026-01-12 20:20:24 +07:00
dependabot[bot]
17b1c04aa0 chore(deps): bump the version-all group in /frontend with 2 updates (#12340)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 15:53:33 +04:00
Hiep Le
624a241bbf refactor: Dockerfile (enterprise) (#12368) 2026-01-12 02:27:44 +07:00
Hiep Le
7862e10f03 chore: update enterprise/Dockerfile (#12355) 2026-01-10 11:00:44 -07:00
Tim O'Farrell
7380039bf6 feat(frontend): improve public share menu behavior (#12345)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-01-10 08:53:22 -07:00
Hiep Le
d773dd6514 feat: implement reCAPTCHA enterprise risk-based non-interactive (#12288) 2026-01-10 22:04:35 +07:00
Xingyao Wang
175117e8b5 Fix: Prevent Enter key from submitting during IME composition (#12252)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-10 11:34:25 +08:00
Tim O'Farrell
778a1cf609 Fix for critical regression where conversations will not start in OSS (#12347) 2026-01-09 19:29:56 +00:00
OpenHands Bot
c08adc87b4 Bump SDK packages to v1.8.1 (#12343)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-10 02:41:01 +08:00
Graham Neubig
434647e4e4 fix: replace deprecated Pydantic .dict() with .model_dump() (#12339)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-09 12:37:58 -05:00
Tim O'Farrell
849ae13118 Fix regression in EventService search (#12342)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-09 10:00:35 -07:00
Tim O'Farrell
180df8ea20 Remove runtime unit tests (#12331) 2026-01-09 09:06:02 -07:00
sp.wack
17791e5e62 fix: restore agenthub import for agent registration (#12341) 2026-01-09 16:00:06 +00:00
Mengxin Zhu
f3aaebdc33 fix(frontend): preserve path prefix in WebSocket URL for proxy deployments (#12284)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2026-01-09 17:39:02 +04:00
Tim O'Farrell
0e4f0c25d7 Fix merge conflict in DB migrations (#12336) 2026-01-09 05:46:43 +00:00
Hiep Le
d4cf1d4590 fix(frontend): tool titles are not displayed for v1 conversations (#12317) 2026-01-09 10:45:42 +07:00
Graham Neubig
9b50d0cb7d chore(deps): update jinja2, tornado, urllib3 (#12330)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-08 22:05:26 -05:00
Tim O'Farrell
5c411e7fc1 Fix circular import in gitlab_sync.py (#12334)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-08 22:44:18 +00:00
Saurya Velagapudi
6442f772a0 Fix: Parse SANDBOX_VOLUMES env var for agent-server volume mounts (#12327)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-08 20:31:27 +00:00
Rohit Malhotra
5fb431bcc5 feat: Implement Slack V1 integration following GitHub V1 pattern (#11825)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-01-08 13:08:11 -07:00
mamoodi
adfabe7659 docs/styles: Minor updates to some docs and some formatting (#12320) 2026-01-08 12:20:57 -05:00
mamoodi
0ddac3879e Update PR template with note on draft PRs (#12315) 2026-01-08 16:55:16 +00:00
Hiep Le
7398737b06 refactor(frontend): align conversation metrics title to the left in the modal (#12310) 2026-01-08 23:09:26 +07:00
Hiep Le
50d9cbac04 refactor(frontend): reduce gap between icon and text in chat status indicator (#12313) 2026-01-08 23:09:16 +07:00
Sarvatarshan Sankar
a40f7bda21 Fix: Prevent Search API Key from resetting when saving other settings (#12243)
Co-authored-by: Sarvatarshan Sankar <sarvatarshansankar20@Sarvatarshans-MacBook-Air.local>
2026-01-08 19:30:06 +04:00
Pranjal Gupta
39f0e6ed94 perf: eliminate slow chown operations in Docker builds (~41min → seconds) (#12256)
Co-authored-by: Pranjal Gupta <19pran@gmail.com>
2026-01-08 09:29:43 -06:00
Hiep Le
6475aa3487 refactor(frontend): remove auth modal and related tests (#12307) 2026-01-08 21:02:02 +07:00
dependabot[bot]
5dea0d22b4 chore(deps): bump the version-all group across 1 directory with 8 updates (#12298)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-08 16:34:30 +04:00
sp.wack
a6e8b819ad refactor(frontend): Remove dead 402 error handling code (#12305) 2026-01-08 11:56:27 +00:00
Tim O'Farrell
c97e7082f7 Making sure verify_repo_provider is_optional so log is debug not error (#12302)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-08 05:15:00 +00:00
Tim O'Farrell
cb9e6fde24 Fix Python deprecation warning: use auth=Auth.Token() instead of login_or_token (#12299)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 21:39:36 -07:00
Tim O'Farrell
828837a969 APP-319: No longer logging error when idle sandboxes are stopped (#12296) 2026-01-07 22:55:57 +00:00
Tim O'Farrell
bbdedf8641 Fix unbound variable and read_text() bugs in event services (#12297)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 22:07:23 +00:00
Graham Neubig
11d1e79506 refactor(enterprise): Remove custom Prometheus metrics app (#12253)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 14:49:50 -05:00
Tim O'Farrell
e485c93119 APP-318 Increase LiteLLM HTTP timeout from 5s to 15s (#12290)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 17:40:52 +00:00
Tim O'Farrell
cddf01b4e9 Fix AttributeError in GoogleCloudSharedEventService: use self.bucket instead of erroneous import (#12289)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 17:03:28 +00:00
Abhay Mishra
6086c0b09d feat(frontend): convert AuthModal to dedicated /login page (#12143) (#12181)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-07 16:46:24 +00:00
Jatoth Adithya Naik
15836c4d4b Replace conversation loading spinner with skeleton cards (#12230)
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-07 15:57:10 +00:00
Tim O'Farrell
20eb9bd3c5 Use the version from the release tag rather than from main (#12286) 2026-01-07 15:50:57 +00:00
Neha Prasad
ba1770ad89 fix: remove query invalidation to prevent stale (#12238) 2026-01-07 19:20:40 +04:00
dependabot[bot]
4b7ce82f71 chore(deps): bump the version-all group in /frontend with 2 updates (#12277)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 13:21:46 +00:00
Hiep Le
5c20724845 fix: db migration (#12282) 2026-01-07 17:29:31 +07:00
Hiep Le
8ddb815a89 refactor(backend): enhance storage and retrieval of blocked domains (#12273) 2026-01-07 13:41:43 +07:00
Engel Nyst
08df955ba7 Clarify OpenHands naming (replace “OSS” wording in docs and backend) (#12235)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 07:24:27 +01:00
Tim O'Farrell
b816d0448b Fix GoogleCloudSharedEventServiceInjector missing bucket_name field and add tests (#12280)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-06 22:27:00 -07:00
Tim O'Farrell
fa974f8106 APP-307 Add Google Cloud Storage-based EventService implementation (#12264)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-06 15:52:07 -07:00
Xingyao Wang
af5c22700c Bump condenser defaults: max_size 120->240 (#12267)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-07 05:43:23 +08:00
Ryanakml
1907ebeaa8 feat: add chat message skeletons and improve routing stability (#12223)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-07 00:29:08 +04:00
Rohit Malhotra
9686ee02f3 V1 GitHub resolver fixes (#12199)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-06 19:33:54 +00:00
HeyItsChloe
d053a3d363 feat(frontend): adding status indicator and unit test (#12111)
Co-authored-by: Chloe <chloe@openhands.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2026-01-06 13:01:27 +00:00
Tim O'Farrell
acc0e893e3 Bump openhands to 1.7.4 (#12269) 2026-01-05 21:40:42 +00:00
Xingyao Wang
a8098505c2 Add litellm_extra_body metadata for V1 conversations (#12266)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-06 03:27:06 +08:00
sp.wack
9b834bf660 feat(frontend): create useAppTitle hook for dynamic document titles (#12224) 2026-01-05 23:17:53 +04:00
Xingyao Wang
5744f6602b Handle expired Keycloak session with user-friendly error message (#12168)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-05 15:04:36 +00:00
Neha Prasad
4a82768e6d feat: add empty state to Changes tab with icon and message (#12237)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2026-01-05 14:22:47 +00:00
Hiep Le
6f86e589c8 feat: allow manual reinstallation for gitlab resolver (#12184) 2026-01-05 12:05:20 +07:00
shanemort1982
5bd8695ab8 feat: Add configurable sandbox host_port and container_url_pattern for remote access (#12255)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-01-04 20:26:16 -07:00
Tim O'Farrell
8c73c87583 Add extra_hosts support to agent-server containers (#12236)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-03 05:41:31 +00:00
Graham Neubig
40c25cd1ce fix: use Auth.Token for PyGithub authentication (#12248)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-02 21:14:16 -05:00
Graham Neubig
2ebde2529d fix: Handle LiteLLM v1.80+ 404 response for new users (#12250)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-02 22:18:47 +00:00
Graham Neubig
cdc42130e1 fix: replace deprecated get_matching_events with search_events (#12249)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-02 21:59:17 +00:00
Graham Neubig
903c047015 Replace deprecated PyPDF2 with pypdf (#12203)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-02 21:47:52 +00:00
Graham Neubig
ee2ad16442 fix: update pythonjsonlogger.jsonlogger to pythonjsonlogger.json (#12247)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-02 16:13:00 -05:00
dependabot[bot]
a96b47e481 chore(deps): bump posthog-js from 1.312.0 to 1.313.0 in /frontend in the version-all group (#12241)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 22:30:44 +04:00
Hiep Le
5a08277184 fix(backend): stabilize gitlab resolver in saas (#12231) 2026-01-03 01:25:28 +07:00
Hiep Le
63d5ceada6 feat(backend): block tld (#12240)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2026-01-03 00:42:22 +07:00
Mohammed Abdulai
1bae1fc4e6 doc: correct Slack channel to #dev-ui-ux (#12239)
Co-authored-by: Mohammed Abdulai <nurud43@gmail.com>
2026-01-02 15:28:08 +01:00
Engel Nyst
15bc78f4c1 Remove VSCode extension integration from OpenHands repo (#12234) 2026-01-01 19:28:05 +01:00
dependabot[bot]
437046f5a4 chore(deps): bump the version-all group in /frontend with 2 updates (#12232)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 19:29:41 +02:00
Cesar Garcia
714459d6eb fix: run stale issues workflow on upstream repository only (#12162)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-12-31 17:50:48 +00:00
Bharath A V
f9b316453d fix: prevent nested buttons in tooltip button (#12177)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-31 16:08:37 +00:00
Ryanakml
96d073ee5b fix(frontend): add missing onClose prop to conversation panel modals (#12219)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-31 15:29:03 +00:00
Osama Mabkhot
f7d416ac8e refactor(frontend): remove HeroUI BaseModal and migrate MetricsModal (#12174)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-31 15:18:58 +00:00
yunbae
b7d5f903cf fix(frontend): Agent Tools & Metadata not available for V1 conversations (#12180)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-31 19:08:09 +04:00
yunbae
2734a5a52d fix(frontend): show stop action button for running or starting conversations (#12215) 2025-12-31 19:07:09 +04:00
dependabot[bot]
51868ffac6 chore(deps): bump @tanstack/react-query from 5.90.15 to 5.90.16 in /frontend in the version-all group (#12225)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-31 14:44:20 +00:00
Aaron Sequeira
4c0f0a1e9b feat: Support Tau-Bench and BFCL evaluation benchmarks (#11953)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-31 03:12:50 +00:00
dependabot[bot]
82e0aa7924 chore(deps): bump ncipollo/release-action from 1.16.0 to 1.20.0 (#11851)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-12-31 03:02:48 +00:00
Eliot Jones
9043aa69d8 refactor: Update expected cygnal output format (#12060) 2025-12-30 22:01:36 -05:00
dependabot[bot]
23d379fa41 build(deps): bump node from 24.8-trixie-slim to 25.2-trixie-slim in /containers/app (#11756)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-12-30 21:18:13 -05:00
Neha Prasad
6f9c0aa3b1 fix: display conversation title in delete confirmation modal (#11818)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-30 20:59:30 -05:00
Xingyao Wang
232dcf4991 fix(ci): update PAT_TOKEN to ALLHANDS_BOT_GITHUB_PAT for enterprise preview (#12216)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-31 04:41:45 +08:00
Hiep Le
ffdd95305f fix(backend): invalid api key (#12217) 2025-12-31 02:05:43 +07:00
sp.wack
bfe8275963 hotfix(test): add top-level mock for custom-toast-handlers in conversation-panel tests (#12220) 2025-12-30 19:04:29 +00:00
OpenHands Bot
06a97fc382 Bump SDK packages to v1.7.3 (#12218)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-12-30 18:47:14 +00:00
Graham Neubig
b5758b1604 Update GithubIntegration to use auth=Auth.AppAuth() (#12204)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-30 12:59:51 -05:00
mamoodi
3ae09680d6 Release 1.1.0 (#12212) 2025-12-30 11:35:14 -05:00
sp.wack
0e5f4325be hotfix(frontend): set terminal background color for xterm.js 6.0.0 compatibility (#12213) 2025-12-30 14:58:58 +00:00
dependabot[bot]
64d4085612 chore(deps): bump the version-all group in /frontend with 2 updates (#12211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-30 18:52:39 +04:00
sp.wack
103e3ead0a hotfix(frontend): validate git changes response is array before mapping (#12208) 2025-12-30 12:33:09 +00:00
dependabot[bot]
d5e83d0f06 chore(deps): bump peter-evans/create-or-update-comment from 4 to 5 (#12192)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-12-29 23:50:40 +00:00
dependabot[bot]
443918af3c chore(deps): bump docker/setup-qemu-action from 3.6.0 to 3.7.0 (#12193)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-30 00:25:56 +01:00
dependabot[bot]
910646d11f chore(deps): bump actions/cache from 4 to 5 (#12191)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-30 00:25:17 +01:00
Engel Nyst
d9d19043f1 chore: Mark V0 legacy files with clear headers and V1 pointers (#12165)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-12-30 00:21:29 +01:00
Graham Neubig
4dec38c7ce fix(event-webhook): Improve error logging with exception type and stack trace (#12202)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-29 18:09:20 -05:00
Graham Neubig
c3f51d9dbe fix(billing): Add error handling for LiteLLM API failures in get_credits (#12201)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-29 23:01:55 +00:00
chuckbutkus
ecbd3ae749 Fix local dev deployments (#12198) 2025-12-29 16:18:02 -05:00
Hiep Le
8ee1394e8c feat: add button to authentication modal to resend verification email (#12179) 2025-12-30 02:12:14 +07:00
Tim O'Farrell
d628e1f20a feat: Add frontend support for public conversation sharing (#12047)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-12-29 12:04:06 -07:00
sp.wack
1480d4acb0 fix(frontend): deduplicate events on WebSocket reconnect (#12197) 2025-12-29 19:03:48 +00:00
Hiep Le
58a70e8b0d fix(backend): preserve users custom llm settings during settings migrations (#12134)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-12-29 23:28:20 +07:00
Hiep Le
49e46a5fa1 refactor(backend): remove <sub> in slack response (#12135) 2025-12-29 23:27:48 +07:00
Hiep Le
2cf6494773 fix(backend): install_gitlab_webhooks.py is not functioning as expected (#12185) 2025-12-29 23:27:31 +07:00
Hiep Le
d3afbfa447 refactor(backend): add description field support for secrets (v1 conversations) (#12080) 2025-12-29 22:43:07 +07:00
Hiep Le
8d69b4066f fix(backend): exception occurs when running the latest code from the main branch (v1 conversations) (#12183) 2025-12-29 09:57:14 -05:00
dependabot[bot]
2261281656 chore(deps): bump @tanstack/react-query from 5.90.12 to 5.90.14 in /frontend in the version-all group (#12189)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 14:33:52 +00:00
sp.wack
d68b2cdd1a hotfix(frontend): fix provider type import (#12187) 2025-12-29 18:01:22 +04:00
dependabot[bot]
c70ecc8fe3 chore(deps): bump the version-all group across 1 directory with 6 updates (#12161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-29 13:54:58 +00:00
Pedro Henrique
a3e85e2c2d test: Add MC/DC tests for loop pattern detector (stuck_detector) (#11600)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-29 14:15:15 +01:00
Hiep Le
3bef4e6c2d refactor(frontend): update the error message for email addresses containing + during signup (#12178) 2025-12-29 19:36:28 +07:00
Engel Nyst
97654e6a5e Configurable conda/mamba channel_alias for runtime builds (#11516)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-29 00:40:57 +01:00
Tim O'Farrell
30114666ad Bump the SDK to 1.7.1 (#12182)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-28 18:57:08 +00:00
dependabot[bot]
ee50f333ba chore(deps): bump actions/upload-artifact from 4 to 5 (#11805)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-12-28 09:51:34 -05:00
dependabot[bot]
09d1748a14 build(deps): bump actions/setup-python from 5 to 6 (#11755)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-12-28 09:49:17 -05:00
dependabot[bot]
81519343c4 chore(deps): bump actions/download-artifact from 4 to 6 (#11524)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-12-28 09:49:02 -05:00
dependabot[bot]
f742811e81 chore(deps): bump actions/setup-node from 4 to 6 (#11442)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-12-28 08:58:26 -05:00
johba
f8e4b5562e Forgejo integration (#11111)
Co-authored-by: johba <admin@noreply.localhost>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: johba <johba@harb.eth>
Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: MrGeorgen <65063405+MrGeorgen@users.noreply.github.com>
Co-authored-by: MrGeorgen <moinl6162@gmail.com>
2025-12-27 15:57:31 -05:00
Tim O'Farrell
cb1d1f8a0d Fix install-hooks CronJob failing when gitlab_webhook table doesn't exist (#12167)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-26 10:53:21 -07:00
Tim O'Farrell
a829d10213 ALL-4634: implement public conversation sharing feature (#12044)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-26 10:02:01 -07:00
Tim O'Farrell
cb8c1fa263 ALL-4627 Database Fixes (#12156)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-26 09:19:51 -07:00
lif
c80f70392f fix(frontend): clean up console warnings in test suite (#12004)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-25 22:26:12 +04:00
Guy Elsmore-Paddock
94e6490a79 Use tini as Docker Runtime Init to Ensure Zombie Processes Get Reaped (#12133)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-12-25 06:16:52 +00:00
Tim O'Farrell
09af93a02a Agent server env override (#12068)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-12-25 03:55:06 +00:00
shanemort1982
5407ea55aa Fix WebSocket localhost bug by passing DOCKER_HOST_ADDR to runtime containers (#12113)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-24 14:26:45 -07:00
Tim O'Farrell
fe1026ee8a Fix for re-creating deleted conversation (#12152) 2025-12-24 12:13:29 -07:00
Tim O'Farrell
6d14ce420e Implement Export feature for V1 conversations with comprehensive unit tests (#12030)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2025-12-24 17:50:57 +00:00
lif
36fe23aea3 fix(llm): retry LiteLLM bad gateway errors (#12117) 2025-12-24 06:37:12 -05:00
sp.wack
9049b95792 docs(frontend): React Router testing guide (#12145) 2025-12-24 14:21:55 +04:00
Hiep Le
e2b2aa52cd feat: require email verification for new signups (#12123) 2025-12-24 14:56:02 +07:00
Tim O'Farrell
dc99c7b62e Fix SQLAlchemy result handling in get_sandbox_by_session_api_key (#12148)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-24 00:11:16 +00:00
Tim O'Farrell
8bc1a47a78 Fix for error in get_sandbox_by_session_api_key (#12147) 2025-12-23 22:18:36 +00:00
Tim O'Farrell
8d0e7a92b8 ALL-4636 Resolution for connection leaks (#12144)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-23 19:02:56 +00:00
Hiep Le
f6e7628bff feat: prevent signups using email addresses with a plus sign and enforce the existing email pattern (#12124) 2025-12-24 01:48:05 +07:00
sp.wack
fae83230ee docs(frontend): Add API services guide for frontend development (#12132) 2025-12-23 12:57:55 +00:00
sp.wack
a9d2f72d72 docs(frontend): Add MSW testing guide for frontend development (#12131) 2025-12-23 16:32:27 +04:00
Tim O'Farrell
2b8f779b65 fix: Runtime pods fail to start due to missing Playwright browser path (#12130) 2025-12-22 17:04:10 +00:00
Hiep Le
10edb28729 fix(frontend): llm settings view resets to basic after saving (#12097) 2025-12-22 23:00:57 +07:00
Hiep Le
5553d3ca2e feat: support blocking specific email domains (#12115) 2025-12-21 19:49:11 +07:00
Graham Neubig
6605070d05 Fix fastmcp stateless_http deprecation warning (#12108)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-12-20 09:45:52 -05:00
Tim O'Farrell
0677cebb25 More efficiency (#12112) 2025-12-19 21:18:48 -07:00
Graham Neubig
fa2567b2a0 Fix xterm dimensions error with explicit checks instead of try-catch (#12095)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-20 01:09:09 +00:00
Graham Neubig
305396550a Fix flaky test_tool_call_validation_error_handling test (#12110)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-19 19:39:53 -05:00
Tim O'Farrell
adff39507a fix: eliminate N+1 performance bug in RemoteSandboxService with batch endpoint (#12105)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-19 23:24:40 +00:00
mamoodi
a873af307a Update CODEOWNERS (#12106) 2025-12-19 14:31:53 -05:00
Tim O'Farrell
800e861b88 Replace manual pagination loops with page_iterator utility function (#12027)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-19 19:17:49 +00:00
sp.wack
0246b1bc43 hotfix(frontend): update websocket handler test mocks to use correct event count endpoint (#12104) 2025-12-19 17:00:59 +00:00
aditya-gaharawar
f870246c3d feat: improve accessibility of served tab buttons (#12103)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-12-19 16:48:34 +00:00
Hiep Le
58340b3ef9 refactor(frontend): consolidate state stores into stores directory (#12100) 2025-12-19 21:12:38 +07:00
dependabot[bot]
107d555445 chore(deps): bump the version-all group in /frontend with 7 updates (#12082)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 16:07:49 +04:00
Engel Nyst
ec9daf3bcc Fix tool call validation error handling for Groq LLM provider (#10927)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-12-19 07:31:07 +01:00
chuckbutkus
88c053b23c Speed up docker build of enterprise server (#12074) 2025-12-18 22:12:01 -05:00
Xingyao Wang
d063ee599b chore: set default model to claude-opus-4-5-20251101 (#12093)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-19 02:50:49 +00:00
Xingyao Wang
c2e4172088 feat: Add sk-oh- prefix to OpenHands Cloud API keys (#12092)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-18 21:06:03 +00:00
Wang Siyuan
d90579b398 fix: make local runtime use host-writable paths and local cache defaults (#12015)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-12-18 17:31:12 +01:00
Xingyao Wang
aff9d69d41 feat(frontend): add prefer-optional-chain ESLint rule and apply fixes (#12073)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-18 22:42:52 +08:00
Hiep Le
afce58a27d refactor(frontend): move frontend/src/ui/microagent-management-service to frontend/src/api (#12017) 2025-12-18 20:27:38 +07:00
Hiep Le
43f7a6fdbd fix(frontend): resolve overlap between conversation panel and account context menu (#12079) 2025-12-18 20:17:32 +07:00
Hiep Le
2ce6c9836e fix: load settings (#12077) 2025-12-18 02:45:32 +00:00
Tim O'Farrell
28dc3be034 Fixed performance bug in remote sandbox service (#12076) 2025-12-18 00:59:57 +00:00
chuckbutkus
2ed5c6073a Add variable to disable vscode plugin (#11812)
Co-authored-by: John-Mason P. Shackelford <jpshack@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-17 11:25:52 -05:00
Hiep Le
9ef11bf930 feat: show available skills for v1 conversations (#12039) 2025-12-17 23:25:10 +07:00
Hiep Le
f98e7fbc49 fix(frontend): observation events and action events (v1 conversations) (#12066)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-17 22:34:28 +07:00
Hiep Le
0607614372 feat(frontend): add refresh button to changes tab (#12036)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-12-17 22:29:18 +07:00
dependabot[bot]
2c83e419dc chore(deps): bump the version-all group across 1 directory with 5 updates (#12071) 2025-12-17 19:16:54 +04:00
Nhan Nguyen
435e537693 fix: Prevent old instructions from being re-executed after conversation condensation (#11982) 2025-12-17 13:05:10 +01:00
Tim O'Farrell
dc14624480 Fix for frontend stall (#12069) 2025-12-17 03:35:46 +00:00
Tim O'Farrell
281ac91540 Bump sdk 1.6.0 (#12067) 2025-12-16 21:53:15 +00:00
Rohit Malhotra
7853b41add Add OAuth 2.0 Device Flow backend for CLI authentication (#11984)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-12-16 11:54:01 -05:00
Tim O'Farrell
2a98c95557 Release OpenHands v1.0.0 (#12052)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-12-16 11:03:29 -05:00
Mariam Saeed
3b7b2fd8cc fix(frontend): Separate pause state from agent loading (#12041)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-16 13:39:15 +00:00
Hiep Le
49740a463f fix(frontend): clicking think block in conversation pane breaks ui (v1 conversations) (#12057) 2025-12-16 20:14:19 +07:00
dependabot[bot]
ee97542080 chore(deps): bump the version-all group in /frontend with 7 updates (#12050)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 23:08:17 +04:00
Tim O'Farrell
9753ad3a48 Removed Legacy Conversation Manager (#12053) 2025-12-15 17:47:21 +00:00
Abhay Mishra
a12170e4c9 refactor(frontend): Extracted useQuery and useMutation from the main branch (#12031)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-12-15 16:37:52 +00:00
Xingyao Wang
5c377f303f Update SWEBench score in README (#12051) 2025-12-15 16:25:31 +00:00
Graham Neubig
089d9c1ee5 Add deprecation warning to evaluation README (#11997)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-16 00:21:13 +08:00
Abhay Mishra
f52d9899e2 Consolidate scattered test files into a unified frontend/__tests__/ directory (#12002) 2025-12-15 19:58:09 +04:00
Neha Prasad
47914c3576 chore: remove pnpm settings from npmrc (#12028) 2025-12-15 12:05:42 +07:00
Hiep Le
67c9b6cf86 refactor(frontend): websocket error message (v1 conversations) (#12045) 2025-12-15 01:31:12 +07:00
Hiep Le
b937d344db fix(backend): initial titles show full uuids instead of shortened uuids (v1 conversations) (#12020) 2025-12-15 00:39:32 +07:00
Hiep Le
f2def8fd7f fix(backend): organizational skills do not trigger (v1 conversations) (#12037) 2025-12-14 23:31:07 +07:00
Hiep Le
eb9a22ef7e fix(backend): unable to use custom mcp servers (v1 conversations) (#12038) 2025-12-14 23:30:49 +07:00
Tim O'Farrell
d57880f849 Agent server image from env (#12003)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-13 08:16:41 -07:00
Hiep Le
d772dd65a5 fix(frontend): fix fetching the number of events on the front end (v1 conversations) (#11987) 2025-12-12 22:10:22 +07:00
dependabot[bot]
5daada17fd chore(deps): bump the version-all group in /frontend with 8 updates (#12022)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 18:54:45 +04:00
Hiep Le
c6a8fc379b feat: support security analyzer settings for v1 conversations (#12008) 2025-12-12 21:49:15 +07:00
Abhay Mishra
5a21c59a3c refactor(frontend): Consolidate duplicate Settings type definitions (#12006) 2025-12-12 14:16:31 +00:00
Bharath A V
976d9d1ab9 Refactor(mocks): modularize MSW handlers into domain-specific files (#11974)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-12-12 13:21:08 +00:00
Bharath A V
6917d45d3a refactor(frontend): consolidate settings navigation items logic into shared custom hook (#11950)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-12 13:20:59 +00:00
Nhan Nguyen
8a202b945b fix(frontend): animate loading spinner for in-progress tasks (#12001)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-12 13:03:28 +00:00
Hiep Le
60491e30e8 chore: update sdk to latest version (#12013) 2025-12-12 00:08:18 +07:00
dependabot[bot]
62f594bc28 chore(deps): bump the version-all group across 1 directory with 2 updates (#12009)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 15:42:57 +00:00
sp.wack
94086f119a fix(frontend): Fix avatar context menu closing on diagonal mouse movement (#11994) 2025-12-11 19:17:21 +04:00
Neha Prasad
f76016aa4d chore: remove unused frontend dependencies (#11940)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-11 15:15:11 +00:00
Lee Kyeong Joon
b16845fa06 refactor(frontend): Refactor tests to use Zustand's native state setting instead of vi.mock for stores (#11980)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-11 14:53:08 +00:00
sp.wack
f4dd5384d0 fix(frontend): Fix empty state showing alongside error in RecentConversations (#11993) 2025-12-11 14:27:49 +00:00
Hiep Le
09e50b876d fix: app unawareness of custom secrets in v1 conversations (#11914) 2025-12-11 14:19:22 +07:00
Tim O'Farrell
f7c3a36745 feat: remember last selected git provider in homepage dropdown (#11979)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-10 19:22:47 -07:00
Rohit Malhotra
a593730b21 Add environment variable kill switch for V1 conversation creation (#11998)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-10 18:25:01 -05:00
Hiep Le
b308307ea2 chore: update sdk to latest version (#11999) 2025-12-11 03:35:01 +07:00
Rohit Malhotra
1d1eb6dcb0 Fix dup conversations for v1 PR comment resolver jobs (#11995) 2025-12-10 18:17:42 +00:00
HeyItsChloe
853547be82 fix(frontend): fix highlighted in terminal is missing background color (#11981)
Co-authored-by: Chloe <chloe@openhands.com>
2025-12-10 20:26:02 +04:00
Hiep Le
272a37d1b8 refactor(frontend): remove certain security analyzer options for v1 conversations (#11988) 2025-12-10 21:50:30 +07:00
sp.wack
92c91471b2 Add GitHub Actions workflow for frontend E2E tests with Playwright (#11990) 2025-12-10 14:43:34 +00:00
dependabot[bot]
7875df4be8 chore(deps): bump the version-all group in /frontend with 2 updates (#11973)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 12:39:30 +00:00
Rohit Malhotra
0aaad16d35 Fix V1 resolver custom secrets validation error (#11976)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-09 16:47:31 +00:00
Vasco Schiavo
df92923959 Refactor return statement to simplify get_supported_llm_models method (#11947) 2025-12-09 16:27:49 +00:00
sp.wack
e18168020a fix(frontend): skip flaky WebSocket broadcast tests (#11972) 2025-12-09 14:57:26 +00:00
Tim O'Farrell
a9c76d0ed4 APP-240 Fix GitHub webhook timeout issue to prevent client disconnect errors (#11965)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-09 07:29:11 -07:00
Rohit Malhotra
3743d10766 Fix: don't double count v1 conversation metadata (#11970) 2025-12-09 09:16:32 -05:00
Alona
9b57a0b14f Remove error icon from ExpandableMessage component (#11964) 2025-12-09 02:50:03 +07:00
Alona
8559efa7b2 Remove loud red x icon on agent commands (#11962) 2025-12-09 01:28:13 +07:00
Hiep Le
bf06b7e3f3 fix(frontend): hide api key input field in advanced settings when provider is openhands (#11949) 2025-12-08 23:03:59 +07:00
Hiep Le
959d610d86 fix(frontend): set v1_enable correctly when saving mcp settings (#11948) 2025-12-08 23:03:26 +07:00
Bharath A V
16125f2ae9 Refactor(frontend): move settings-service into api folder and update (#11958) 2025-12-08 14:50:51 +00:00
dependabot[bot]
d31950c061 chore(deps): bump the version-all group in /frontend with 6 updates (#11957)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 14:46:08 +00:00
Tim O'Farrell
db64abc580 Refactor webhook endpoints to use session API key authentication (#11926)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-08 07:40:01 -07:00
Rohit Malhotra
ed7adb335c GitHub V1 Callbacks not trigger by v1 enabled flag (#11923)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-08 03:58:45 +00:00
Cesar Garcia
584517edec docs: fix broken architecture diagram link in openhands/README.md (#11924)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-12-07 22:15:58 +00:00
Tim O'Farrell
1a983d2978 APP-190 Add browser screenshot support for V1 conversations (#11919)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-12-06 14:04:01 -07:00
Hiep Le
d7b36c9579 fix: switching from own model to breaks functionality (#11916) 2025-12-06 11:21:18 +07:00
Tim O'Farrell
72c7d9c497 APP-216 Support multiple git providers in conversation secrets (#11908)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-05 11:50:45 -07:00
Hiep Le
7811a62491 refactor(frontend): remove max_budget_per_task input element for v1 (#11921) 2025-12-06 00:50:40 +07:00
dependabot[bot]
4344f5ad4e chore(deps): bump the version-all group across 1 directory with 9 updates (#11915)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 14:59:19 +00:00
Neha Prasad
17821f782e feat: display command in observation block (#11885)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-12-05 14:45:28 +00:00
Neha Prasad
e1b283886f fix: conversation tab state sync across browser tabs (#11680)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-05 14:35:10 +00:00
chuckbutkus
1d9cf72e39 JPMC Modifications (#11882) 2025-12-04 23:32:20 -05:00
Hiep Le
59ca8bd9a8 refactor: derive deterministic key id from secret itself (#11905) 2025-12-05 01:41:32 +07:00
Tim O'Farrell
3a9aa90c3a Bumped SDK to V1.4.1 (#11903) 2025-12-04 17:52:27 +00:00
sp.wack
0a98f165e2 chore(frontend): isolate MAJOR dependency bumps from #11869 (#11887)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-04 18:16:55 +04:00
Neha Prasad
6ec477dae2 fix: enable terminal scrollback to view command history (#11883) 2025-12-04 17:29:42 +07:00
Hiep Le
d0496fea8c chore: update sdk to latest version (#11897)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-12-04 01:36:19 +07:00
Tim O'Farrell
8f91db8ec4 Replace USE_V1_CONVERSATION_API feature flag with user setting (#11893)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-03 09:02:40 -07:00
sp.wack
816d8acf1f chore(frontend): isolate PATCH dependency bumps from #11869 (#11890)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-03 19:55:35 +04:00
sp.wack
97e6cb1340 chore(frontend): isolate MINOR dependency bumps from #11869 (#11888)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-03 14:29:18 +00:00
Hiep Le
cd9a3b02cf feat(frontend): display command in observation block in ui (#11884) 2025-12-03 19:55:55 +07:00
Marco Dalalba
14695a8f0e refactor/bugfix: simplify hasOpenHandsSuffix with provider lookup for gitlab/azure (#11877)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-12-03 12:34:21 +00:00
Hiep Le
eaea8b3ce1 fix(frontend): buying credits does not work on staging (#11873) 2025-12-03 10:07:01 +07:00
Tim O'Farrell
72555e0f1c APP-193: add X-Access-Token header support to get_api_key_from_header (#11872)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-02 17:01:09 -07:00
Hiep Le
fd13c91387 fix(backend): apply user-defined condenser_max_size in new v1 conversations (#11862) 2025-12-03 00:24:25 +07:00
Hiep Le
6139e39449 fix(backend): git settings not applying in v1 conversations (#11866) 2025-12-02 21:34:37 +07:00
Hiep Le
f76ac242f0 fix(backend): conversation statistics are currently not being persisted to the database (V1). (#11837) 2025-12-02 21:22:02 +07:00
Hiep Le
1f9350320f refactor(frontend): hide agent dropdown when v1 is enabled (#11860) 2025-12-02 20:22:40 +07:00
Hiep Le
1a3460ba06 fix(frontend): image attachments not working in v1 conversations (#11864) 2025-12-02 20:22:14 +07:00
Tim O'Farrell
8f361b3698 Fix git checkout error in workspace setup (#11855)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-01 23:01:30 +00:00
Tim O'Farrell
fd6e0cab3f Fix V1 MCP services (Fix tavily search) (#11840)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-01 21:19:19 +00:00
Hiep Le
33eec7cb09 feat(frontend): automatically scroll to bottom of container on plan content update (#11808)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-12-01 16:23:48 +00:00
Hiep Le
6c2862ae08 feat(frontend): add handler for 'create a plan' button click (#11806) 2025-12-01 11:08:00 -05:00
Hiep Le
6c821ab73e fix(frontend): the content of the FinishObservation event is not being rendered correctly. (#11846) 2025-12-01 09:29:18 -05:00
sp.wack
96f13b15e7 Revert "chore(backend): Add better PostHog tracking" (#11749) 2025-12-01 13:58:03 +00:00
Hiep Le
d9731b6850 feat(frontend): show plan content in the planning tab (#11807) 2025-12-01 08:42:44 -05:00
Hiep Le
e7e49c9110 fix(frontend): AppConversationStartTask timezone display in ui (#11847) 2025-12-01 08:13:54 -05:00
Ray Myers
27590497d5 chore: update posthog-js from 1.290.0 to 1.298.1 (#11830)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-12-01 17:03:44 +04:00
adshrc
991f1a242c feat(llm): added Claude Opus 4.5 model and corresponding test (#11841) 2025-12-01 11:09:33 +00:00
Marco Dalalba
6d8cca43a8 fix: add Azure GPT-5 family to stop words unsupported patterns (#11842) 2025-12-01 01:32:34 +01:00
Hiep Le
d62bb81c3b feat(backend): implement API to fetch contents of PLAN.md (#11795) 2025-11-30 13:29:13 +07:00
Hiep Le
156d0686c4 fix(frontend): the content of the BrowserObservation event is not being rendered correctly (#11832) 2025-11-28 23:16:34 +07:00
Hiep Le
d0b1d29379 fix(backend): the SaaS codebase is currently non-functional. (#11834) 2025-11-28 09:12:02 -07:00
Jeffrey Ma
974bcdfd0b SWE-fficiency benchmark implementation (#11716)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: enyst <engel.nyst@gmail.com>
2025-11-27 09:13:15 +01:00
Rohit Malhotra
ed094b6a97 Fix v1_enabled migration failures by making column nullable (#11829)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-26 21:41:03 +00:00
Rohit Malhotra
49624219ed fix(migration): add server_default to v1_enabled column migration (#11828)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-26 20:21:12 +00:00
Rohit Malhotra
9906a1d49a V1: Support v1 conversations in github resolver (#11773)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-26 13:11:05 -05:00
Hiep Le
014884333d fix(frontend): Remove azure devops integration button from cloud settings (#11826) 2025-11-27 00:41:28 +07:00
Hiep Le
865ddaabdf fix(backend): unable to start a new V0 conversation (#11824) 2025-11-26 23:49:52 +07:00
Hiep Le
3219834e35 fix(frontend): resolve issue preventing cost from displaying (V1) (#11798) 2025-11-26 19:39:07 +07:00
Hiep Le
2e295073ae fix(frontend): fileeditorobservationevent rendering issue (#11820) 2025-11-26 18:40:28 +07:00
Hiep Le
5ef45cfec2 refactor(frontend): support TerminalObservation event (#11819) 2025-11-26 17:53:47 +07:00
Tim O'Farrell
d737141efa SDK Fixes (#11813)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-26 10:44:17 +00:00
Hiep Le
b532a5e7fe fix(backend): github token not working for v1 conversations (#11814) 2025-11-26 01:04:45 +07:00
Hiep Le
c58e2157ea feat(frontend): display skill ready for v1 conversations (#11815) 2025-11-25 23:37:54 +07:00
mamoodi
9cc8687271 fix: handle None return from version_info.get('Components') in docker builder (#11816)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-25 15:35:40 +00:00
aoi127
f6e4d00df1 fix: prevent newline accumulation in XML parameter serialization (#11767)
Co-authored-by: Lai Jinyi <laijinyi@tp-link.com.cn>
2025-11-25 11:56:35 +01:00
Engel Nyst
7782f2afe9 Fix links in readme (#11802) 2025-11-24 19:58:55 +01:00
Hiep Le
639de8114f feat(frontend): add blue border to Planning Agent events (#11788) 2025-11-24 21:36:30 +07:00
Hiep Le
b830d1c513 fix(frontend): hide api key field for openhands provider and auto-populate the key (#11791) 2025-11-24 20:44:15 +07:00
Wan Arif
3504ca7752 feat: add Azure DevOps integration support (#11243)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-11-22 14:00:24 -05:00
Graham Neubig
1e513ad63f feat: Add configurable stuck/loop detection (#11799)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@all-hands.dev>
2025-11-21 22:27:38 +00:00
chuckbutkus
b9b8d27135 Add config option to check if roles are present (#11414) 2025-11-21 16:56:19 -05:00
mamoodi
da8a4b1179 remove unused workflows (#11793) 2025-11-20 16:21:37 -05:00
Hiep Le
d1d08bc490 feat(frontend): integration of events from execution and planning agents within a single conversation (#11786) 2025-11-20 21:21:46 +07:00
Tim O'Farrell
c82e183066 Fix Docker hostname issues in HTTP requests (#11787)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-20 11:59:58 +00:00
Rohit Malhotra
26e7d8060f fix(migrations): make SETTING_UP_SKILLS enum migration idempotent (#11782)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-11-20 11:21:40 +00:00
Tim O'Farrell
ba883ffeca Feat sandbox skills (#11785) 2025-11-20 10:52:13 +00:00
Rodney A.
77b565ce08 fix(frontend): fix duplicate React Aria IDs by updating @heroui/react to v2.8.5 (#11783) 2025-11-20 11:48:11 +07:00
Hiep Le
151c2895e0 feat(frontend): disable change-agent button until WebSocket connection is ready (#11781) 2025-11-20 01:28:17 +07:00
Tim O'Farrell
9538c7bd89 fix(migrations): add SETTING_UP_SKILLS to appconversationstarttaskstatus enum (#11780)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-19 18:14:24 +00:00
Boxuan Li
790b7c6e39 Add grok-code-fast-1 to FUNCTION_CALLING_PATTERNS (#11775)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-19 08:38:57 -05:00
Daniel Foguelman
4c57a98660 Remove inconsistent parameters in claude sonnet (#11719) 2025-11-19 08:38:19 -05:00
Hiep Le
28af600c16 fix(frontend): display LLM configuration errors to the user (#11776) 2025-11-19 20:15:42 +07:00
Hiep Le
36cf4e161a fix(backend): ensure microagents are loaded for V1 conversations (#11772)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-11-19 18:54:08 +07:00
Engel Nyst
bede37fdb6 feat: Enable native tool calling for gemini-3-pro-preview (#11774) 2025-11-18 23:29:54 +01:00
Rohit Malhotra
1a33606987 Chore: move CLI code its own repo (#11724) 2025-11-18 19:59:12 +00:00
Robert Brennan
494eba094f Update fundraising amount in COMMUNITY.md (#11771) 2025-11-18 17:31:34 +00:00
Tim O'Farrell
84c62c4f23 Bumped Software Agent SDK and fixed V1 Delete (#11768) 2025-11-18 15:52:23 +00:00
Hiep Le
f5611c2188 fix(frontend): terminal output not appearing in v1 (#11769) 2025-11-18 22:03:28 +07:00
Robert Brennan
492c12693d Update README and COMMUNITY.md for v1 (#11747)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-18 09:37:30 -05:00
Graham Neubig
5345716340 Fix the favicon (#11766) 2025-11-18 07:30:46 -05:00
Hiep Le
b43f7439a7 feat(backend): enable deletion of sub-conversations when removing a parent conversation (#11757) 2025-11-18 17:53:04 +07:00
Tim O'Farrell
192a8e6de4 Fix for docker regression (#11759) 2025-11-17 18:18:40 +00:00
Hiep Le
cd87987037 feat(frontend): add functionality to fetch sub-conversation data (#11758) 2025-11-18 00:49:54 +07:00
Graham Neubig
0dbf09f954 Update OpenHands logos with new branding (#11741)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-17 12:47:36 -05:00
Tim O'Farrell
871cc932d7 APP-155 Made all version tags the same color to reduce confusion (#11753) 2025-11-17 16:05:27 +00:00
மனோஜ்குமார் பழனிச்சாமி
60c4d9a23f Add Groq models to function calling patterns (#11745) 2025-11-17 09:19:39 -05:00
Tim O'Farrell
6c121bde74 APP-159 Fix Docker container networking for agent server URLs (#11751)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-17 06:09:21 -07:00
sp.wack
6dcf27dbc0 feat(frontend): move PostHog trackers to the frontend (#11748) 2025-11-17 14:55:29 +04:00
Tim O'Farrell
1f6ef8175b Enhance Docker image pull logging with periodic progress updates (#11750)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-17 03:15:21 -07:00
Hiep Le
d6fab190bf feat(frontend): integrate with the API to create a sub-conversation for the planning agent (#11730) 2025-11-15 09:43:21 +07:00
Hiep Le
833aae1833 feat(backend): exclude sub-conversations when searching for conversations (#11733) 2025-11-15 00:21:27 +07:00
Tim O'Farrell
2841e35f24 Do not get live status updates when they are not required (#11727)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-14 07:55:43 -07:00
Tim O'Farrell
8115d82f96 feat: add created_at__gte filter to search_app_conversation_start_tasks (#11740)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-14 07:08:34 -07:00
Hiep Le
7263657937 feat(backend): include sub-conversation ids when fetching conversation details (#11734) 2025-11-14 11:34:30 +07:00
jpelletier1
34fcc50350 Update to include llms.txt (#11737) 2025-11-13 21:42:50 +00:00
jpelletier1
24a9758434 Adding an Agent Builder Skill/Microagent (#11720) 2025-11-13 16:10:00 -05:00
Tim O'Farrell
f24d2a61e6 Fix for wrong column name (#11735) 2025-11-13 17:55:23 +00:00
Hiep Le
e3d0380c2e feat(frontend): add support for the shift + tab shortcut to cycle through conversation modes (#11731) 2025-11-14 00:10:25 +07:00
Hiep Le
8c3f93ddc4 feat(frontend): set descriptive text for all options in the change agent button (#11732) 2025-11-14 00:10:15 +07:00
Hiep Le
bc86796a67 feat(backend): enable sub-conversation creation using a different agent (#11715) 2025-11-13 23:06:44 +07:00
sp.wack
d5b2d2ebc5 fix(frontend): Sync client PostHog opt-in status with server setting (#11728) 2025-11-13 13:22:05 +00:00
Rohit Malhotra
b605c96796 Hotfix: rm max condenser size override (#11713) 2025-11-12 20:13:16 -05:00
sp.wack
8192184d3e chore(backend): Add better PostHog tracking (#11655)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-12 16:47:21 +00:00
Hiep Le
8e75f25108 feat(frontend): implement new task tracker interface (#11692) 2025-11-12 22:59:45 +07:00
Neha Prasad
73fe865c7e feat: queue chat messages during runtime connection (#11687)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-11-12 13:20:09 +00:00
Rohit Malhotra
95a44f4248 CLI release 1.0.7 (#11712) 2025-11-11 16:46:30 -05:00
Rohit Malhotra
0a6b76ca2d CLI: bump agent-sdk (#11710)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-11 20:29:18 +00:00
Tim O'Farrell
8b6521de62 Fix for issue where conversation does not start (#11695) 2025-11-11 20:23:18 +00:00
mamoodi
11636edf15 Release 0.62.0 (#11706) 2025-11-11 14:57:13 -05:00
Hiep Le
915c180ba7 feat(frontend): disable change agent button while agent is running (#11691) 2025-11-12 00:46:12 +07:00
sp.wack
cdd8aace86 refactor(frontend): migrate from direct posthog imports to usePostHog hook (#11703) 2025-11-11 15:48:56 +00:00
Hiep Le
a2c312d108 feat(frontend): add plan preview component (#11676) 2025-11-11 21:59:23 +07:00
sp.wack
5ad3572810 chore(frontend): Remove user_activated PostHog capture event (#11704) 2025-11-11 14:35:04 +00:00
John Eismeier
967e9e1891 Propose fix some typos and ignore emacs backup files (#11701)
Signed-off-by: John E <jeis4wpi@outlook.com>
2025-11-11 09:20:42 -05:00
sp.wack
f8a41d3ffe fix(frontend): Properly reflect default user analytics setting (#11702) 2025-11-11 18:19:37 +04:00
John-Mason P. Shackelford
6e9e7547e5 Add Documentation link to profile context menu (#11583)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-11 09:16:32 -05:00
Hiep Le
9b4f1c365b feat(frontend): add change agent button (#11675) 2025-11-11 20:28:48 +07:00
Engel Nyst
f4dcc136d0 tests: remove Windows-only tests and clean up Windows conditionals (#11697) 2025-11-10 21:34:55 +01:00
Rohit Malhotra
36a8cbbfe4 Add GitHub CI workflow to check package versions (#11637)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-10 19:39:49 +00:00
Engel Nyst
83a3c2c5bf Add invisible AI-only guidance to Checklist: humans must fill (#11688) 2025-11-10 18:13:18 +00:00
Engel Nyst
63c9e6403f ci: remove flaky Windows Python tests workflow (#11694) 2025-11-10 12:43:48 -05:00
Hiep Le
bff734070c feat(frontend): update data-placeholder when switching to plan mode (#11674) 2025-11-10 21:30:29 +04:00
mamoodi
5db6bffaf6 Add some notes to the README for things that are not officially suppo… (#11663) 2025-11-10 20:16:41 +04:00
Engel Nyst
14807ed273 ci: remove outdated integration runner (#11653) 2025-11-10 15:51:40 +01:00
Rohit Malhotra
e0d26c1f4e CLI: custom visualizer (#11677) 2025-11-07 19:45:01 +00:00
Rohit Malhotra
27c8c330f4 CLI release 1.0.6 (#11672) 2025-11-07 14:10:04 -05:00
sp.wack
0c927b19d2 fix(frontend): agent loading condition update logic (#11673) 2025-11-07 18:04:27 +00:00
Hiep Le
a660321d55 feat(frontend): display plan content within the planner tab (#11658) 2025-11-08 00:54:15 +07:00
Tim O'Farrell
0e94833d5b Now removing V1 sandboxes in the V0 endpoint (#11671) 2025-11-07 10:51:46 -07:00
Engel Nyst
b83e2877ec CLI: align with agent-sdk renames (#11643)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
2025-11-07 11:30:37 -05:00
sp.wack
7acee16de5 fix(frontend): Consider start task job error status for loading indicators (#11670) 2025-11-07 19:24:29 +04:00
sp.wack
1e3f1de773 fix(frontend): Add translations for error status' (#11669) 2025-11-07 13:51:58 +00:00
sp.wack
bfe60d3bbf chore(frontend): Disable /feedback/conversation/{conversationId}/batch for V1 conversations (#11668) 2025-11-07 13:50:09 +00:00
sp.wack
ad75cd05d8 chore(frontend): Add better PostHog tracking (#11645) 2025-11-07 16:35:54 +04:00
Hiep Le
955f87561b feat(frontend): enable pinning and unpinning of conversation tabs (#11659) 2025-11-07 13:38:30 +07:00
Hiep Le
1e5bff82f2 feat(frontend): visually highlight chat input container in plan mode (#11647) 2025-11-07 13:14:28 +07:00
Tim O'Farrell
ddf58da995 Fix V1 callbacks (#11654)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-06 16:05:58 -07:00
Hiep Le
b678d548c2 feat(frontend): create new planner tab in the interface (#11646) 2025-11-06 23:56:35 +07:00
Hiep Le
a1d4d62f68 feat(frontend): show server status menu when hovering over the status indicator (#11635) 2025-11-06 16:23:08 +04:00
Yakshith
75e54e3552 fix(llm): remove default reasoning_effort; fix Gemini special case (#11567) 2025-11-05 23:30:46 +01:00
Yuxiao Cheng
6b211f3b29 Fix stuck after incorrect TaskTrackingAction (#11436)
Co-authored-by: jarrycyx <dzdzzd@126.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-11-05 22:09:51 +00:00
mamoodi
e208b64a95 Update free credits statement to $10 (#11651) 2025-11-05 20:57:56 +00:00
mamoodi
555444f239 Release 0.61.0 (#11618)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
2025-11-05 15:11:22 -05:00
Tim O'Farrell
d99c7827d8 More updates of agent_status to execution_status (#11642)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-05 19:19:34 +00:00
mamoodi
5a8f08b4ef Remove obsolete workflow (#11650) 2025-11-05 19:56:34 +01:00
Hiep Le
44fbd6c1b9 refactor(backend): the delete_app_conversation_info function (#11648) 2025-11-05 23:45:16 +07:00
sp.wack
7e824ca5dc fix(frontend): V1 Loading UI (#11630) 2025-11-05 14:23:10 +00:00
sp.wack
9a7002d817 fix(frontend): V1 resume conversation / agent (#11627) 2025-11-05 14:16:46 +00:00
Hiep Le
6411d4df94 feat(frontend): display text label when items are selected across all canvas views (#11636) 2025-11-05 16:47:22 +07:00
eddierichter-amd
c544ea1187 localhost base_url fixup when running in a docker container (#11474)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-11-04 17:57:25 -05:00
Graham Neubig
308d0e62ab Change error logging to info for missing config files (#11639) 2025-11-04 21:27:13 +01:00
Ray Myers
9abd1714b9 fix - Speed up runtime tests (#11570)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-04 11:17:55 -06:00
sp.wack
f1abe6c6af fix(ci): Lint Python (#11634) 2025-11-04 16:24:24 +00:00
Tim O'Farrell
30b5ad1768 Fix for issue where conversations won't start (#11633) 2025-11-04 08:51:22 -07:00
Hiep Le
4ea3e4b1fd refactor(frontend): break down conversation service into smaller services (#11594) 2025-11-04 20:52:44 +07:00
Hiep Le
7049a3e918 chore(frontend): add feature flag for planning agent (#11616) 2025-11-04 20:32:45 +07:00
Hiep Le
fa431fb956 refactor(backend): update get_microagent_management_conversations API to support V1 (#11313)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-11-04 17:44:44 +07:00
Tim O'Farrell
2fc8ab2601 Bumped Software Agent SDK (#11626) 2025-11-03 14:53:12 -07:00
mamoodi
8e119c68ab Create CNAME 2025-11-03 15:43:34 -05:00
Hiep Le
8893f9364d refactor: update delete_app_conversation to accept ID instead of object (#11486)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-11-03 13:26:33 -07:00
Tim O'Farrell
727520f6ce V1 CORS Fix (#11586)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 12:14:02 -07:00
Tim O'Farrell
898c3501dd Update initial from $20 to $10 (#11624) 2025-11-03 19:11:18 +00:00
Jessica Kerr
4c81965c61 build(devcontainer): add uvx installation (#11610) 2025-11-03 19:37:54 +01:00
Hiep Le
0f054c740c fix(frontend): the width of the branch dropdown appears inconsistent on medium-sized screens. (#11620) 2025-11-04 01:30:11 +07:00
Yuxiao Cheng
9bcf80dba5 Adding error logging when config file is not found. (#11419)
Co-authored-by: jarrycyx <dzdzzd@126.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-11-03 13:19:48 -05:00
மனோஜ்குமார் பழனிச்சாமி
2a98cd9338 Fix import order for Windows PowerShell support (#11557) 2025-11-03 13:14:23 -05:00
Rohit Malhotra
b31dbfc21a CLI: make sure MCP server doesn't persist even after removal (#11602)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 12:45:47 -05:00
Tim O'Farrell
5d711d5576 Exclude V1 conversations from V0 (#11595) 2025-11-03 09:57:34 -07:00
Rohit Malhotra
3eb73de924 CLI: lazy load conversation for /new command (#11601)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:30:08 +00:00
Rohit Malhotra
2e49f07451 CLI: Rm loading context (#11603)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:15:47 +00:00
Hiep Le
e51685dab4 fix(frontend): there is insufficient padding below the code block. (#11615) 2025-11-03 21:34:01 +07:00
Aphix
b85cc0c716 fix: Autodetect pwsh.exe & DLL path (Win/non-WSL) (#11044) 2025-11-03 08:27:30 -05:00
Hiep Le
7ef1720b5d fix(frontend): correct handling of OBSERVATION_MESSAGE messages for task events (#11613) 2025-11-03 18:57:11 +07:00
Hiep Le
a6385b4059 fix(frontend): agent status shows “Disconnected” when starting a new conversation until sandbox initializes (#11612) 2025-11-03 18:56:52 +07:00
sp.wack
7cfe667a3f fix(frontend): V1 event rendering to display thought + action, then thought + observation (#11596) 2025-11-03 14:07:35 +04:00
Engel Nyst
6e8be827b8 Fix deprecated links (#11605) 2025-11-01 12:37:32 -04:00
Tim O'Farrell
2ccc611e7c Regenerated poetry lock to update dependencies (#11593) 2025-10-31 20:25:01 +00:00
Rohit Malhotra
1f7dec4d94 CLI: patch release 1.0.5 (#11598) 2025-10-31 19:57:39 +00:00
sp.wack
966e4ae990 APP-125: Reset V1 terminal state when switching conversations by forcing remount (#11592)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-31 18:41:19 +00:00
Rohit Malhotra
231019974c CLI: fix binary build (#11591) 2025-10-31 18:01:29 +00:00
Rohit Malhotra
d246ab1a21 Hotfix(CLI): make settings page available even when conversation hasn't been created (#11588)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-31 17:19:53 +00:00
jpelletier1
15c207c401 Disables Copilot icon by default (#11589) 2025-10-31 17:06:15 +00:00
Rohit Malhotra
cf21cfed6c Hotfix(CLI): make sure to update condenser credentials (#11587)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-31 16:37:59 +00:00
Rohit Malhotra
12d57df6ac CLI Patch release 1.0.4 (#11585) 2025-10-31 14:59:39 +00:00
Rohit Malhotra
3239eb4027 Hotfix(CLI): Update README to use V1 CLI for serve command and point to new docker image artifacts (#11584) 2025-10-31 09:34:19 -04:00
Rohit Malhotra
9be673d553 CLI: Create conversation last minute (#11576)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-10-30 23:04:41 +00:00
Tim O'Farrell
7272eae758 Fix remote sandbox permissions (#11582) 2025-10-30 22:13:02 +00:00
mamoodi
ec670cd130 Rename LLM API Key to OpenHands LLM Key in settings (#11577)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-30 16:52:31 -04:00
Hiep Le
31702bf46b fix(frontend): delays in updating conversation titles before they are reflected in the user interface. (#11558)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-10-30 18:06:18 +00:00
Tim O'Farrell
5894d2675e V1 IDs without hyphens (#11564) 2025-10-30 16:33:16 +00:00
Hiep Le
59a992c0fb feat(frontend): allow all users to access the LLM page and disable Pro subscription functionality (#11573) 2025-10-30 22:01:30 +07:00
Rohit Malhotra
1939bd0fda CLI Release 1.0.3 (#11574) 2025-10-30 14:39:42 +00:00
Ray Myers
58e690ef75 Fix flaky test_condenser_metrics_included by creating new action objects (#11555)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-30 09:20:06 -05:00
Rohit Malhotra
97403dfbdb CLI: rename deprecated args (#11568) 2025-10-30 09:20:27 -04:00
sp.wack
2fc31e96d0 chore(frontend): Add V1 git service API with unified hooks for git changes and diffs (#11565) 2025-10-30 13:03:25 +00:00
Rohit Malhotra
6558b4f97d CLI: bump agent-sdk version (#11566)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-30 03:38:36 +00:00
Kevin Musgrave
12d6da8130 feat(evaluation): Filter task ids by difficulty for SWE Gym rollouts (#11490)
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-30 02:30:19 +00:00
mamoodi
38f2728cfa Release 0.60.0 (#11544)
Co-authored-by: rohitvinodmalhotra@gmail.com <rohitvinodmalhotra@gmail.com>
2025-10-29 16:17:46 -04:00
sp.wack
fab48fe864 chore(frontend): Remove Jupyter tab and features (#11563) 2025-10-29 17:57:48 +00:00
sp.wack
a196881ab0 chore(frontend): Make terminal read-only by removing user input handlers (#11546) 2025-10-29 21:30:10 +04:00
Rohit Malhotra
ca2c9546ad CLI: add unit test for default agent (#11562)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-29 13:11:06 -04:00
sp.wack
704fc6dd69 chore(frontend): Add history loading state for V1 conversations (#11536) 2025-10-29 16:11:25 +00:00
Hiep Le
6630d5dc4e fix(frontend): display error content when FileEditorAction encounters an error (#11560) 2025-10-29 20:03:25 +04:00
Hiep Le
0e7fefca7e fix(frontend): displaying observation result statuses (#11559) 2025-10-29 20:02:32 +04:00
sp.wack
4020448d64 chore(frontend): Add unified hooks for V1 sandbox URLs (VSCode and served hosts) (#11511)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-29 14:52:31 +00:00
Hiep Le
2fdd4d084a feat(frontend): display “waiting for user confirmation” when agent status is “awaiting_user_confirmation” (#11539) 2025-10-29 17:31:05 +04:00
Hiep Le
aba5d54a86 feat(frontend): V1 confirmation's call the right API (#11542) 2025-10-29 17:29:27 +04:00
sp.wack
6710a39621 hotfix(frontend): add unified conversation config hook with V1 support (#11547) 2025-10-29 17:26:37 +04:00
Tim O'Farrell
fccc6f3196 Fix permissions issue in docker Sandbox (#11549) 2025-10-28 20:24:54 +00:00
Tim O'Farrell
7447cfdb3d Removed the pyright tool setting because it degrades VSCode developer experience (#11545) 2025-10-28 18:31:07 +00:00
Rohit Malhotra
297af05d53 Remove V0 CLI (#11538) 2025-10-28 13:16:07 -04:00
Hiep Le
b8f387df94 fix(frontend): chat suggestions disappear when “Push” is pressed before V1 conversation starts (#11494) 2025-10-29 00:04:30 +07:00
sp.wack
fc67f39b74 feat(frontend): implement V1 conversation pause/resume functionality (#11541) 2025-10-28 19:45:40 +04:00
Ray Myers
bc8922d3f9 chore - Remove trixie image build (#11533) 2025-10-28 15:32:48 +00:00
Hiep Le
37d58bba4d fix(frontend): the microagent management page is currently broken as a result of recent V1 changes. (#11522) 2025-10-28 22:10:13 +07:00
sp.wack
037a2dca8f fix(frontend): render terminal input commands and skip empty outputs (#11537) 2025-10-28 14:32:19 +00:00
Hiep Le
b5920eece6 fix(frontend): unable to create a new conversation through the Microagent Management page when the feature flag is enabled. (#11523) 2025-10-28 16:25:56 +04:00
sp.wack
a81bef8cdf chore: Bump agent server (#11520)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-28 16:21:19 +04:00
Ray Myers
450aa3b527 fix(llm): support draft editor retries by adding correct_num to LLMConfig (#11530)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Justin Coffi <jcoffi+github@gmail.com>
2025-10-28 01:02:50 +00:00
Ray Myers
4decd8b3e9 Provide httpx default context for OS-provided certs (#11505)
Co-authored-by: Pierrick Hymbert <pierrick.hymbert@gmail.com>
2025-10-27 17:54:20 -05:00
Zacharias Fisches
818f743dc7 Bugfix: respect config.tom system_prompt_filename when running swe-bench (#11091)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-10-27 21:55:05 +00:00
Evelyn Colon
f402371b27 Contribution to Ignoring SSL Errors (#11230)
Co-authored-by: Evelyn Colon <evelyncolon13579@gmail.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-10-27 21:29:55 +00:00
Nick Ludwig
92b1fca719 feat: Add option to pass custom kwargs to litellm.completion (#11423)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-10-27 21:07:31 +00:00
Yakshith
8de13457c3 fix(docker): mark /app as safe git directory to resolve pre-commit er… (#10988)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-10-27 20:26:34 +00:00
Alex42006
8f94b68ea1 Fix red X when Tavily MCP does not return error (#11227)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-10-27 21:36:08 +04:00
Rohit Malhotra
eb616dfae4 Refactor: rename user secrets table to custom secrets (#11525)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-27 16:58:07 +00:00
John-Mason P. Shackelford
26c636d63e OpenHands Enterprise Telemetry Service M1 (#11468)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-10-27 13:01:56 +00:00
sp.wack
3ec8d70d04 fix(frontend): Optimistically cache individual conversations from paginated results (#11510) 2025-10-27 16:24:46 +04:00
John-Mason P. Shackelford
694ac74bb9 chore: repo.md now has instructions for enterprise directory (#11478)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-27 07:45:04 -04:00
Cesar Garcia
7ee20067a8 Fix broken DOC_STYLE_GUIDE.md link in Development.md (#11368)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-10-26 14:25:42 -04:00
Tim O'Farrell
054c5b666f Moved event search to background thread (#11487) 2025-10-26 09:39:27 -06:00
PiteXChen
0ff7329424 Optimize the condense conditions of the condenser (#11332)
Signed-off-by: CLFutureX <chenyongqyl@163.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-10-26 11:23:22 -04:00
Wolf Noble
86c590cdc3 feat: Expose session_id to sandbox/runtime container (#10863) 2025-10-26 11:21:38 -04:00
mamoodi
319677e629 Fix README docker image (#11515) 2025-10-26 11:16:24 -04:00
Robert Brennan
f8b566b858 Fix broken docker links (#11514) 2025-10-26 11:05:44 -04:00
Hiep Le
f9694858fb fix(frontend): frontend connects to WebSocket too early (#11493) 2025-10-26 12:35:55 +04:00
Hiep Le
7880c39ede fix(frontend): loading spinner shown while waiting for start task to complete (#11492) 2025-10-26 12:29:21 +04:00
Robert Brennan
b5e00f577c Replace All-Hands-AI references with OpenHands (#11287)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-10-26 01:52:45 +02:00
Rohit Malhotra
2631294e79 Fix: incorrect attribute in convo info service (#11503) 2025-10-24 16:33:36 -06:00
Ray Myers
47776ae2ad chore - Reference new org in python deps (#11504) 2025-10-24 20:56:56 +00:00
Graham Neubig
0ad411e162 Fix: Change default DOCKER_ORG from all-hands-ai to openhands (#11489)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-24 15:06:48 -04:00
Alona
7bc56e0d74 feat: add 'git' as trigger word for bitbucket microagent (#11499) 2025-10-24 18:49:50 +00:00
Samuel Akerele
e450a3a603 fix(llm): Support nested paths in litellm_proxy/ model names (#11430)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-10-24 17:41:25 +00:00
softpudding
17e32af6fe Enhance dead-loop recovery by pausing agent and reprompting (#11439)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-24 11:25:14 +00:00
Tim O'Farrell
4b303ec9b4 Fixes to unblock frontend (#11488)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-10-23 14:43:45 -06:00
Ray Myers
eb954164a5 chore - update ghcr enterprise build to new org 2025-10-23 12:53:01 -05:00
Tim O'Farrell
0c1c2163b1 The AsyncRemoteWorkspace class was moved to the SDK (#11471) 2025-10-23 09:39:56 -06:00
Hiep Le
dd2a62c992 refactor(frontend): disable some agent server API until implemented in the server source code (#11476) 2025-10-23 19:38:18 +04:00
Rohit Malhotra
f3d9faef34 SAAS: dedup fetching user settings from keycloak id (#11480)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-23 09:56:55 -04:00
Hiep Le
134c122026 fix: disable pro subscription upgrade on LLM page for self-hosted installs (#11479) 2025-10-23 01:11:04 +07:00
Rohit Malhotra
523b40dbfc SAAS: drop deprecated table (#11469)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-22 10:52:10 -04:00
sp.wack
6a5b915088 Add unified file upload support for V0 and V1 conversations (#11457) 2025-10-22 17:44:38 +04:00
sp.wack
a5c5133961 Remove queries from cache and do not refetch them after starting a conversation (#11453) 2025-10-22 13:42:09 +00:00
sp.wack
eea1e7f4e1 Prevent calling V1 "start tasks” API if feature flag is disabled + always set “start tasks” query cache to stale (#11454) 2025-10-22 20:38:32 +07:00
Hiep Le
e2d990f3a0 feat(backend): implement get_remote_runtime_config support for V1 conversations (#11466) 2025-10-22 15:38:25 +07:00
Hiep Le
f258eafa37 feat(backend): add support for updating the title in V1 conversations (#11446) 2025-10-22 13:36:56 +07:00
Hiep Le
19634f364e fix(backend): repository pill does not display the selected repository when a conversation is initiated via slack (#11225) 2025-10-22 13:12:32 +07:00
Alona
aa6446038c fix: remove accidentally committed Docker image tags from config.sh (#11470) 2025-10-22 04:48:17 +00:00
Tim O'Farrell
dbddc1868e Fixes for VSCode code completion (#11449) 2025-10-21 21:39:50 +00:00
Rohit Malhotra
cd967ef4bc SAAS: add local development helper scripts (#11459) 2025-10-21 21:26:23 +00:00
Tim O'Farrell
e34c13ea3c Set dump mode to json to convert UUIDs to strings (#11467) 2025-10-21 19:20:56 +00:00
Hiep Le
1f35a73cc4 fix(frontend): display repository information after creating a V1 conversation (#11463) 2025-10-21 18:24:26 +00:00
Alona
267528fa82 fix: refresh provider tokens proactively and update git URLs on resume (#11296) 2025-10-22 01:19:08 +07:00
sp.wack
49f360d021 Fix toast dismissal to target specific toast IDs instead of all toasts (#11455) 2025-10-21 17:43:14 +00:00
sp.wack
9520da668c Prevent WebSocket provider remount by defaulting to V1 (#11458) 2025-10-21 17:11:15 +00:00
Rohit Malhotra
9d19292619 V1: Experiment manager (#11388)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-21 16:04:48 +00:00
sp.wack
fc9a87550d Fix zero state not showing for V1 conversations (#11452) 2025-10-21 20:04:01 +04:00
sp.wack
490d3dba10 Remove toast notifications for starting/resuming conversation sandbox (#11456) 2025-10-21 20:03:45 +04:00
Rohit Malhotra
5ed1dde2e9 CLI Patch Release 1.0.2 (#11448) 2025-10-21 15:32:00 +00:00
sp.wack
a68576b876 Clear conversation state when switching between V1 conversations (#11447) 2025-10-21 20:21:58 +07:00
mamoodi
722124ae83 Move Search API Key and Confirmation Mode to Advanced settings (#11390)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-21 08:51:21 -04:00
Tim O'Farrell
44578664ed Add Concurrency Limits to SandboxService (#11399)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-20 20:22:12 +00:00
Rohit Malhotra
9efe6eb776 Simplify security analyzer confirmation: replace two reject options with single 'Reject' option (#11443)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-10-20 19:45:42 +00:00
Tim O'Farrell
6d137e883f Add VSCode URL support and worker ports to sandbox services (#11426)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-20 18:43:08 +00:00
Xingyao Wang
2889f736d9 Use PyPI version of Agent-SDK (#11411) 2025-10-20 17:25:54 +00:00
sp.wack
531683abae feat(frontend): V1 conversation API (PARTIAL) (#11336)
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-10-20 20:57:40 +04:00
Ryan H. Tran
fab64a51b7 Add support for claude-haiku-4-5 (#11434) 2025-10-20 19:56:40 +07:00
Rohit Malhotra
cc18a18874 [Hotfix, V1 CLI]: Include missing condenser prompt template in binary executable (#11428)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-19 18:18:23 +00:00
Graham Neubig
7525a95af0 Fix excessive error logging for missing org-level microagents (#11425)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-19 13:41:28 -04:00
Rohit Malhotra
640f50d525 Fix: exception handling for get convo metadata (#11421) 2025-10-17 18:12:18 +00:00
mamoodi
6f2f85073d Update PR template (#11420) 2025-10-17 13:57:42 -04:00
jpelletier1
9f3b2425ec Experimental first-time user onboarding microagent (#11413) 2025-10-17 12:35:24 -04:00
Tim O'Farrell
1ebc3ab04e Fix FastMCP authentication API breaking change (#11416)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-17 16:32:36 +00:00
Graham Neubig
9bd0566e4e fix(logging): Prevent LiteLLM logs from leaking through root logger (#11356)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-17 11:19:22 -04:00
Engel Nyst
d82972e126 FE: Replace AllHands logo with OpenHands logo (#11417)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-17 11:44:56 +02:00
Boxuan Li
e1b94732a8 Implement graceful shutdown for headless mode (#11401)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-16 23:09:31 -07:00
olyashok
5219f85bfa feat: make websocket client wait timeout configurable (#11405)
Co-authored-by: Alex <alex@cellect.ai>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-10-16 16:49:50 +00:00
Kevin Musgrave
a237b578c0 feat(evaluation): Add multi-swe-bench dependency and fix rollout script (#11326)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-10-16 14:35:19 +00:00
mogith-pn
f42a4f75cb feat: Clarifai Integration as LLM Provider (#11324) 2025-10-16 18:23:00 +04:00
Engel Nyst
3e645f8649 fix(integration-tests): accept --eval-num-workers and --eval-note in integration test runner (#11387) 2025-10-16 09:50:24 -04:00
Ryan H. Tran
5182388323 Extend context truncation cases (#11393) 2025-10-16 17:55:57 +07:00
juanmichelini
471d272c7c Mint security eval fix (#11273)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-10-16 01:42:05 +00:00
Tim O'Farrell
0522734875 Add ProcessSandboxService implementation for process-based sandboxes (#11394)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-15 17:53:50 -06:00
Tim O'Farrell
f4fd8ea907 Added flag to disable the V1 endpoints inside nested V0 runtimes (#11391) 2025-10-15 15:33:52 -06:00
Engel Nyst
e9413aaded Update header logo branding to OpenHands (#11383)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-15 21:28:22 +02:00
sp.wack
ef004962cc hotfix(backend): Update route parameters from 'id' to 'sandbox_id' (#11389) 2025-10-15 16:40:10 +00:00
Hiep Le
58d67a2480 fix(backend): repository search is not working in the production environment (#11386) 2025-10-15 23:24:27 +07:00
Tim O'Farrell
72179f45d3 Fir for broken V1 db connection (#11382) 2025-10-15 08:07:43 -04:00
Ray Myers
15e7709ff6 chore - Add README notice of coming org rename (#11381) 2025-10-14 23:39:12 -05:00
Christopher Pereira
bb563d6dd1 Fix typos (#11162) 2025-10-14 14:01:51 -04:00
Hiep Le
d991b9880d fix(frontend): reo tracker should be available only in the SaaS environment, not in self-hosted instances (#11367) 2025-10-14 22:16:45 +07:00
Rohit Malhotra
fe82cfd277 Hotfix(CLI VI): unable to launch via default entrypoint (#11354) 2025-10-14 10:39:49 -04:00
Cesar Garcia
16fa8ea7be Fix broken logo in README.md (#11366) 2025-10-14 14:29:27 +00:00
Tim O'Farrell
f292f3a84d V1 Integration (#11183)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-10-14 02:16:44 +00:00
1681 changed files with 136352 additions and 118198 deletions

1
.devcontainer/README.md Normal file
View File

@@ -0,0 +1 @@
This way of running OpenHands is not officially supported. It is maintained by the community.

View File

@@ -7,5 +7,8 @@ git config --global --add safe.directory "$(realpath .)"
# Install `nc`
sudo apt update && sudo apt install netcat -y
# Install `uv` and `uvx`
wget -qO- https://astral.sh/uv/install.sh | sh
# Do common setup tasks
source .openhands/setup.sh

12
.github/CODEOWNERS vendored
View File

@@ -1,12 +1,8 @@
# 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 code owners
/frontend/ @amanape
/openhands-ui/ @amanape
# Evaluation code owners
/frontend/ @amanape @hieptl
/openhands-ui/ @amanape @hieptl
/openhands/ @tofarr @malhotra5 @hieptl
/enterprise/ @chuckbutkus @tofarr @malhotra5
/evaluation/ @xingyaoww @neubig
# Documentation code owners
/docs/ @mamoodi

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!

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

@@ -1,12 +1,38 @@
- [ ] This change is worth documenting at https://docs.all-hands.dev/
- [ ] Include this change in the Release Notes. If checked, you **must** provide an **end-user friendly** description for your change below
<!-- If you are still working on the PR, please mark it as draft. Maintainers will review PRs marked ready for review, which leads to lost time if your PR is actually not ready yet. Keep the PR marked as draft until it is finally ready for review -->
**End-user friendly description of the problem this fixes or functionality this introduces.**
## Summary of PR
<!-- Summarize what the PR does -->
---
**Summarize what the PR does, explaining any non-trivial design decisions.**
## 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. -->
---
**Link of any specific issues this addresses:**
## Change Type
<!-- Choose the types that apply to your PR -->
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Refactor
- [ ] Other (dependency update, docs, typo fixes, etc.)
## 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. -->
- [ ] 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.

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
import os
import re
import sys
def find_version_references(directory: str) -> tuple[set[str], set[str]]:
openhands_versions = set()
runtime_versions = set()
version_pattern_openhands = re.compile(r'openhands:(\d{1})\.(\d{2})')
version_pattern_runtime = re.compile(r'runtime:(\d{1})\.(\d{2})')
for root, _, files in os.walk(directory):
# Skip .git directory and docs/build directory
if '.git' in root or 'docs/build' in root:
continue
for file in files:
if file.endswith(
('.md', '.yml', '.yaml', '.txt', '.html', '.py', '.js', '.ts')
):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find all openhands version references
matches = version_pattern_openhands.findall(content)
if matches:
print(f'Found openhands version {matches} in {file_path}')
openhands_versions.update(matches)
# Find all runtime version references
matches = version_pattern_runtime.findall(content)
if matches:
print(f'Found runtime version {matches} in {file_path}')
runtime_versions.update(matches)
except Exception as e:
print(f'Error reading {file_path}: {e}', file=sys.stderr)
return openhands_versions, runtime_versions
def main():
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
print(f'Checking version consistency in {repo_root}')
openhands_versions, runtime_versions = find_version_references(repo_root)
print(f'Found openhands versions: {sorted(openhands_versions)}')
print(f'Found runtime versions: {sorted(runtime_versions)}')
exit_code = 0
if len(openhands_versions) > 1:
print('Error: Multiple openhands versions found:', file=sys.stderr)
print('Found versions:', sorted(openhands_versions), file=sys.stderr)
exit_code = 1
elif len(openhands_versions) == 0:
print('Warning: No openhands version references found', file=sys.stderr)
if len(runtime_versions) > 1:
print('Error: Multiple runtime versions found:', file=sys.stderr)
print('Found versions:', sorted(runtime_versions), file=sys.stderr)
exit_code = 1
elif len(runtime_versions) == 0:
print('Warning: No runtime version references found', file=sys.stderr)
sys.exit(exit_code)
if __name__ == '__main__':
main()

View File

@@ -13,12 +13,9 @@ 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.all-hands.dev/all-hands-ai/runtime:${SHORT_SHA}-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:${SHORT_SHA}-nikolaik \
--name openhands-app-${SHORT_SHA} \
docker.all-hands.dev/all-hands-ai/openhands:${SHORT_SHA}"
# Define the uvx command
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/All-Hands-AI/OpenHands@${BRANCH_NAME}#subdirectory=openhands-cli openhands"
docker.openhands.dev/openhands/openhands:${SHORT_SHA}"
# Get the current PR body
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq .body)
@@ -37,11 +34,6 @@ GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
else
@@ -57,11 +49,6 @@ GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
fi

View File

@@ -0,0 +1,65 @@
name: Check Package Versions
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
check-package-versions:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Check for any 'rev' fields in pyproject.toml
run: |
python - <<'PY'
import sys, tomllib, pathlib
path = pathlib.Path("pyproject.toml")
if not path.exists():
print("❌ ERROR: pyproject.toml not found")
sys.exit(1)
try:
data = tomllib.loads(path.read_text(encoding="utf-8"))
except Exception as e:
print(f"❌ ERROR: Failed to parse pyproject.toml: {e}")
sys.exit(1)
poetry = data.get("tool", {}).get("poetry", {})
sections = {
"dependencies": poetry.get("dependencies", {}),
}
errors = []
print("🔍 Checking for any dependencies with 'rev' fields...\n")
for section_name, deps in sections.items():
if not isinstance(deps, dict):
continue
for pkg_name, cfg in deps.items():
if isinstance(cfg, dict) and "rev" in cfg:
msg = f" ✖ {pkg_name} in [{section_name}] uses rev='{cfg['rev']}' (NOT ALLOWED)"
print(msg)
errors.append(msg)
else:
print(f" • {pkg_name}: OK")
if errors:
print("\n❌ FAILED: Found dependencies using 'rev' fields:\n" + "\n".join(errors))
print("\nPlease use versioned releases instead, e.g.:")
print(' my-package = "1.0.0"')
sys.exit(1)
print("\n✅ SUCCESS: No 'rev' fields found. All dependencies are using proper versioned releases.")
PY

View File

@@ -1,69 +0,0 @@
# Workflow that cleans up outdated and old workflows to prevent out of disk issues
name: Delete old workflow runs
# This workflow is currently only triggered manually
on:
workflow_dispatch:
inputs:
days:
description: 'Days-worth of runs to keep for each workflow'
required: true
default: '30'
minimum_runs:
description: 'Minimum runs to keep for each workflow'
required: true
default: '10'
delete_workflow_pattern:
description: 'Name or filename of the workflow (if not set, all workflows are targeted)'
required: false
delete_workflow_by_state_pattern:
description: 'Filter workflows by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually'
required: true
default: "ALL"
type: choice
options:
- "ALL"
- active
- deleted
- disabled_inactivity
- disabled_manually
delete_run_by_conclusion_pattern:
description: 'Remove runs based on conclusion: action_required, cancelled, failure, skipped, success'
required: true
default: 'ALL'
type: choice
options:
- 'ALL'
- 'Unsuccessful: action_required,cancelled,failure,skipped'
- action_required
- cancelled
- failure
- skipped
- success
dry_run:
description: 'Logs simulated changes, no deletions are performed'
required: false
jobs:
del_runs:
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
actions: write
contents: read
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: ${{ github.event.inputs.days }}
keep_minimum_runs: ${{ github.event.inputs.minimum_runs }}
delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }}
delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }}
delete_run_by_conclusion_pattern: >-
${{
startsWith(github.event.inputs.delete_run_by_conclusion_pattern, 'Unsuccessful:')
&& 'action_required,cancelled,failure,skipped'
|| github.event.inputs.delete_run_by_conclusion_pattern
}}
dry_run: ${{ github.event.inputs.dry_run }}

View File

@@ -1,114 +0,0 @@
# Workflow that builds and tests the CLI binary executable
name: CLI - Build binary and optionally release
# Run on pushes to main branch and CLI tags, and on pull requests when CLI files change
on:
push:
branches:
- main
tags:
- "*-cli"
pull_request:
paths:
- "openhands-cli/**"
permissions:
contents: write # needed to create releases or upload assets
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
build-binary:
name: Build binary executable
strategy:
matrix:
include:
# Build on Ubuntu 22.04 for maximum GLIBC compatibility (GLIBC 2.31)
- os: ubuntu-22.04
platform: linux
artifact_name: openhands-cli-linux
# Build on macOS for macOS users
- os: macos-15
platform: macos
artifact_name: openhands-cli-macos
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: openhands-cli
run: |
uv sync
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
echo "Full output:"
cat output.log
if grep -q "❌" output.log; then
echo "❌ Found failure marker in output"
exit 1
fi
echo "✅ Build & test finished without ❌ markers"
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: openhands-cli/dist/openhands*
retention-days: 30
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: build-binary
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release assets
run: |
mkdir -p release-assets
# Copy binaries with appropriate names for release
if [ -f artifacts/openhands-cli-linux/openhands ]; then
cp artifacts/openhands-cli-linux/openhands release-assets/openhands-linux
fi
if [ -f artifacts/openhands-cli-macos/openhands ]; then
cp artifacts/openhands-cli-macos/openhands release-assets/openhands-macos
fi
ls -la release-assets/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-assets/*
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,23 +0,0 @@
name: Dispatch to docs repo
on:
push:
branches: [main]
paths:
- 'docs/**'
workflow_dispatch:
jobs:
dispatch:
runs-on: ubuntu-latest
strategy:
matrix:
repo: ["All-Hands-AI/docs"]
steps:
- name: Push to docs repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.ALLHANDS_BOT_GITHUB_PAT }}
repository: ${{ matrix.repo }}
event-type: update
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "module": "openhands", "branch": "main"}'

View File

@@ -27,7 +27,7 @@ jobs:
poetry-version: 2.1.3
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'poetry'
@@ -38,7 +38,7 @@ jobs:
sudo apt-get install -y libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 xauth xvfb libgbm1 libasound2t64 netcat-openbsd
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
@@ -192,7 +192,7 @@ jobs:
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: playwright-report
path: tests/e2e/test-results/
@@ -200,7 +200,7 @@ jobs:
- name: Upload OpenHands logs
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: openhands-logs
path: |

View File

@@ -43,7 +43,7 @@ jobs:
⚠️ This PR contains **migrations**
- name: Comment warning on PR
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}

View File

@@ -23,7 +23,7 @@ jobs:
- name: Trigger remote job
run: |
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
-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/All-Hands-AI/deploy/actions/workflows/deploy.yaml/dispatches
https://api.github.com/repos/OpenHands/deploy/actions/workflows/deploy.yaml/dispatches

47
.github/workflows/fe-e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# Workflow that runs frontend e2e tests with Playwright
name: Run Frontend E2E Tests
on:
push:
branches:
- main
pull_request:
paths:
- "frontend/**"
- ".github/workflows/fe-e2e-tests.yml"
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
fe-e2e-test:
name: FE E2E Tests
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
node-version: [22]
fail-fast: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: useblacksmith/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
working-directory: ./frontend
run: npm ci
- name: Install Playwright browsers
working-directory: ./frontend
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
working-directory: ./frontend
run: npx playwright test --project=chromium
- name: Upload Playwright report
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30

View File

@@ -37,16 +37,13 @@ jobs:
shell: bash
id: define-base-images
run: |
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
]')
else
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ghcr.io/all-hands-ai/python-nodejs:python3.13-nodejs22-trixie", tag: "trixie" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
]')
fi
@@ -66,7 +63,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
uses: docker/setup-qemu-action@v3.7.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
@@ -88,7 +85,7 @@ jobs:
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Image
name: Build Runtime Image
runs-on: blacksmith-8vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
@@ -104,7 +101,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
uses: docker/setup-qemu-action@v3.7.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
@@ -126,7 +123,7 @@ jobs:
- 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 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
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
@@ -151,6 +148,9 @@ jobs:
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
@@ -163,7 +163,7 @@ jobs:
context: containers/runtime
- name: Upload runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
@@ -200,7 +200,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/all-hands-ai/enterprise-server
images: ghcr.io/openhands/enterprise-server
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -249,156 +249,20 @@ jobs:
- name: Trigger remote job
run: |
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
-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/All-Hands-AI/deploy/actions/workflows/deploy.yaml/dispatches
https://api.github.com/repos/OpenHands/deploy/actions/workflows/deploy.yaml/dispatches
# Run unit tests with the Docker runtime Docker images as root
test_runtime_root:
name: RT Unit Tests (Root)
needs: [ghcr_build_runtime, define-matrix]
runs-on: blacksmith-8vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Download runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
# Forked repos can't push to GHCR, so we need to rebuild using cache
- 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:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- 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 INSTALL_PLAYWRIGHT=0
- name: Run docker runtime tests
shell: bash
run: |
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
# Install to be able to retry on failures for flaky tests
poetry run pip install pytest-rerunfailures
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
# Setting RUN_AS_OPENHANDS to false means use root.
# That should mean SANDBOX_USER_ID is ignored but some tests do not check for RUN_AS_OPENHANDS.
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=false \
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
env:
DEBUG: "1"
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
name: RT Unit Tests (openhands)
runs-on: blacksmith-8vcpu-ubuntu-2204
needs: [ghcr_build_runtime, define-matrix]
strategy:
matrix:
base_image: ${{ fromJson(needs.define-matrix.outputs.base_image) }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Download runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
# Forked repos can't push to GHCR, so we need to rebuild using cache
- 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:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- 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,test,runtime INSTALL_PLAYWRIGHT=0
- name: Run runtime tests
shell: bash
run: |
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
# Install to be able to retry on failures for flaky tests
poetry run pip install pytest-rerunfailures
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=true \
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
env:
DEBUG: "1"
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
# "All Runtime Tests Passed" is a required job for PRs to merge
# Due to this bug: https://github.com/actions/runner/issues/2566, we want to create a job that runs when the
# prerequisites have been cancelled or failed so merging is disallowed, otherwise Github considers "skipped" as "success"
# We can remove this once the config changes
runtime_tests_check_success:
name: All Runtime Tests Passed
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh]
steps:
- name: All tests passed
run: echo "All runtime tests have passed successfully!"
runtime_tests_check_fail:
name: All Runtime Tests Passed
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh]
steps:
- name: Some tests failed
run: |
echo "Some runtime tests failed or were cancelled"
exit 1
update_pr_description:
name: Update PR Description
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'

View File

@@ -1,199 +0,0 @@
name: Run Integration Tests
on:
pull_request:
types: [labeled]
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: true
default: ''
schedule:
- cron: '30 22 * * *' # Runs at 10:30pm UTC every day
env:
N_PROCESSES: 10 # Global configuration for number of parallel processes for evaluation
jobs:
run-integration-tests:
if: github.event.label.name == 'integration-test' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: "read"
id-token: "write"
pull-requests: "write"
issues: "write"
strategy:
matrix:
python-version: ["3.12"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Node.js
uses: useblacksmith/setup-node@v5
with:
node-version: '22.x'
- name: Comment on PR if 'integration-test' label is present
if: github.event_name == 'pull_request' && github.event.label.name == 'integration-test'
uses: KeisukeYamashita/create-comment@v1
with:
unique: false
comment: |
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime,evaluation
- name: Configure config.toml for testing with Haiku
env:
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 10
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Build environment
run: make build
- name: Run integration test evaluation for Haiku
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'haiku_run'
# get integration tests report
REPORT_FILE_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/*haiku*_maxiter_10_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE: $REPORT_FILE_HAIKU"
echo "INTEGRATION_TEST_REPORT_HAIKU<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_HAIKU >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Wait a little bit
run: sleep 10
- name: Configure config.toml for testing with DeepSeek
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 10
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DeepSeek
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'deepseek_run'
# get integration tests report
REPORT_FILE_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/deepseek*_maxiter_10_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE: $REPORT_FILE_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing VisualBrowsingAgent (DeepSeek)
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 15
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for VisualBrowsingAgent (DeepSeek)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD VisualBrowsingAgent '' 15 $N_PROCESSES "t05_simple_browsing,t06_github_pr_browsing.py" 'visualbrowsing_deepseek_run'
# Find and export the visual browsing agent test results
REPORT_FILE_VISUALBROWSING_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/VisualBrowsingAgent/deepseek*_maxiter_15_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_VISUALBROWSING_DEEPSEEK: $REPORT_FILE_VISUALBROWSING_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_VISUALBROWSING_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create archive of evaluation outputs
run: |
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
- name: Upload evaluation results as artifact
uses: actions/upload-artifact@v4
id: upload_results_artifact
with:
name: integration-test-outputs-${{ github.run_id }}-${{ github.run_attempt }}
path: integration_tests_*.tar.gz
- name: Get artifact URLs
run: |
echo "ARTIFACT_URL=${{ steps.upload_results_artifact.outputs.artifact-url }}" >> $GITHUB_ENV
- name: Set timestamp and trigger reason
run: |
echo "TIMESTAMP=$(date +'%Y-%m-%d-%H-%M')" >> $GITHUB_ENV
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "TRIGGER_REASON=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "TRIGGER_REASON=manual-${{ github.event.inputs.reason }}" >> $GITHUB_ENV
else
echo "TRIGGER_REASON=nightly-scheduled" >> $GITHUB_ENV
fi
- name: Comment with results and artifact link
id: create_comment
uses: KeisukeYamashita/create-comment@v1
with:
# if triggered by PR, use PR number, otherwise use 9745 as fallback issue number for manual triggers
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || 9745 }}
unique: false
comment: |
Trigger by: ${{ github.event_name == 'pull_request' && format('Pull Request (integration-test label on PR #{0})', github.event.pull_request.number) || (github.event_name == 'workflow_dispatch' && format('Manual Trigger: {0}', github.event.inputs.reason)) || 'Nightly Scheduled Run' }}
Commit: ${{ github.sha }}
**Integration Tests Report (Haiku)**
Haiku LLM Test Results:
${{ env.INTEGRATION_TEST_REPORT_HAIKU }}
---
**Integration Tests Report (DeepSeek)**
DeepSeek LLM Test Results:
${{ env.INTEGRATION_TEST_REPORT_DEEPSEEK }}
---
**Integration Tests Report VisualBrowsing (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}
---
Download testing outputs (includes both Haiku and DeepSeek results): [Download](${{ steps.upload_results_artifact.outputs.artifact-url }})

View File

@@ -72,34 +72,3 @@ jobs:
- name: Run pre-commit hooks
working-directory: ./enterprise
run: pre-commit run --all-files --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
lint-cli-python:
name: Lint CLI python
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up python
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: "pip"
- name: Install pre-commit
run: pip install pre-commit==4.2.0
- name: Run pre-commit hooks
working-directory: ./openhands-cli
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
# Check version consistency across documentation
check-version-consistency:
name: Check version consistency
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Set up python
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
- name: Run version consistency check
run: .github/scripts/check_version_consistency.py

View File

@@ -1,70 +0,0 @@
# Workflow that checks MDX format in docs/ folder
name: MDX Lint
# Run on pushes to main and on pull requests that modify docs/ files
on:
push:
branches:
- main
paths:
- 'docs/**/*.mdx'
pull_request:
paths:
- 'docs/**/*.mdx'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
mdx-lint:
name: Lint MDX files
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
with:
node-version: 22
- name: Install MDX dependencies
run: |
npm install @mdx-js/mdx@3 glob@10
- name: Validate MDX files
run: |
node -e "
const {compile} = require('@mdx-js/mdx');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
async function validateMDXFiles() {
const files = glob.sync('docs/**/*.mdx');
console.log('Found', files.length, 'MDX files to validate');
let hasErrors = false;
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
await compile(content);
console.log('✅ MDX parsing successful for', file);
} catch (err) {
console.error('❌ MDX parsing failed for', file, ':', err.message);
hasErrors = true;
}
}
if (hasErrors) {
console.error('\\n❌ Some MDX files have parsing errors. Please fix them before merging.');
process.exit(1);
} else {
console.log('\\n✅ All MDX files are valid!');
}
}
validateMDXFiles();
"

View File

@@ -89,7 +89,7 @@ jobs:
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Upgrade pip
@@ -118,7 +118,7 @@ jobs:
contains(github.event.review.body, '@openhands-agent-exp')
)
)
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
@@ -201,7 +201,7 @@ jobs:
issue_number: ${{ env.ISSUE_NUMBER }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `[OpenHands](https://github.com/All-Hands-AI/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
body: `[OpenHands](https://github.com/OpenHands/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
});
- name: Install OpenHands
@@ -233,7 +233,7 @@ jobs:
if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) {
console.log("Installing experimental OpenHands...");
await exec.exec("pip install git+https://github.com/all-hands-ai/openhands.git");
await exec.exec("pip install git+https://github.com/openhands/openhands.git");
} else {
console.log("Installing from requirements.txt...");
@@ -269,7 +269,7 @@ jobs:
fi
- name: Upload output.jsonl as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always() # Upload even if the previous steps fail
with:
name: resolver-output

View File

@@ -48,7 +48,10 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime
run: |
poetry install --with dev,test,runtime
poetry run pip install pytest-xdist
poetry run pip install pytest-rerunfailures
- name: Build Environment
run: make build
- name: Run Unit Tests
@@ -56,48 +59,18 @@ jobs:
env:
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
- name: Run Runtime Tests with CLIRuntime
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -n 5 --reruns 2 --reruns-delay 3 -s tests/runtime/test_bash.py --cov=openhands --cov-branch
env:
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-openhands
path: |
.coverage.${{ matrix.python_version }}
.coverage.runtime.${{ matrix.python_version }}
include-hidden-files: true
# Run specific Windows python tests
test-on-windows:
name: Python Tests on Windows
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- name: Install pipx
run: pip install pipx
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/runtime/utils/test_windows_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
DEBUG: "1"
- name: Run Windows runtime tests with LocalRuntime
run: $env:TEST_RUNTIME="local"; poetry run pytest -svv tests/runtime/test_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
TEST_RUNTIME: local
DEBUG: "1"
test-enterprise:
name: Enterprise Python Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -122,63 +95,17 @@ jobs:
env:
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-enterprise
path: ".coverage.enterprise.${{ matrix.python_version }}"
include-hidden-files: true
# Run CLI unit tests
test-cli-python:
name: CLI Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
matrix:
python-version: ["3.12"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: ./openhands-cli
run: |
uv sync --group dev
- name: Run CLI unit tests
working-directory: ./openhands-cli
env:
# write coverage to repo root so the merge step finds it
COVERAGE_FILE: "${{ github.workspace }}/.coverage.openhands-cli.${{ matrix.python-version }}"
run: |
uv run pytest --forked -n auto -s \
-p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark \
tests --cov=openhands_cli --cov-branch
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-openhands-cli
path: ".coverage.openhands-cli.${{ matrix.python-version }}"
include-hidden-files: true
coverage-comment:
name: Coverage Comment
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [test-on-linux, test-enterprise, test-cli-python]
needs: [test-on-linux, test-enterprise]
permissions:
pull-requests: write
@@ -186,15 +113,12 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
id: download
with:
pattern: coverage-*
merge-multiple: true
- name: Create symlink for CLI source files
run: ln -sf openhands-cli/openhands_cli openhands_cli
- name: Coverage comment
id: coverage_comment
uses: py-cov-action/python-coverage-comment-action@v3

View File

@@ -10,7 +10,6 @@ on:
type: choice
options:
- app server
- cli
default: app server
push:
tags:
@@ -39,36 +38,3 @@ jobs:
run: ./build.sh
- name: publish
run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
release-cli:
name: Publish CLI to PyPI
runs-on: ubuntu-latest
# Run when manually dispatched for "cli" OR for tag pushes that contain '-cli'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'cli')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Build CLI package
working-directory: openhands-cli
run: |
# Clean dist directory to avoid conflicts with binary builds
rm -rf dist/
uv build
- name: Publish CLI to PyPI
working-directory: openhands-cli
run: |
uv publish --token ${{ secrets.PYPI_TOKEN_OPENHANDS }}

View File

@@ -1,135 +0,0 @@
# Run evaluation on a PR, after releases, or manually
name: Run Eval
# Runs when a PR is labeled with one of the "run-eval-" labels, after releases, or manually triggered
on:
pull_request:
types: [labeled]
release:
types: [published]
workflow_dispatch:
inputs:
branch:
description: 'Branch to evaluate'
required: true
default: 'main'
eval_instances:
description: 'Number of evaluation instances'
required: true
default: '50'
type: choice
options:
- '1'
- '2'
- '50'
- '100'
reason:
description: 'Reason for manual trigger'
required: false
default: ''
env:
# Environment variable for the master GitHub issue number where all evaluation results will be commented
# This should be set to the issue number where you want all evaluation results to be posted
MASTER_EVAL_ISSUE_NUMBER: ${{ vars.MASTER_EVAL_ISSUE_NUMBER || '0' }}
jobs:
trigger-job:
name: Trigger remote eval job
if: ${{ (github.event_name == 'pull_request' && (github.event.label.name == 'run-eval-1' || github.event.label.name == 'run-eval-2' || github.event.label.name == 'run-eval-50' || github.event.label.name == 'run-eval-100')) || github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.head_ref || (github.event_name == 'workflow_dispatch' && github.event.inputs.branch) || github.ref }}
- name: Set evaluation parameters
id: eval_params
run: |
REPO_URL="https://github.com/${{ github.repository }}"
echo "Repository URL: $REPO_URL"
# Determine branch based on trigger type
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
EVAL_BRANCH="${{ github.head_ref }}"
echo "PR Branch: $EVAL_BRANCH"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
EVAL_BRANCH="${{ github.event.inputs.branch }}"
echo "Manual Branch: $EVAL_BRANCH"
else
# For release events, use the tag name or main branch
EVAL_BRANCH="${{ github.ref_name }}"
echo "Release Branch/Tag: $EVAL_BRANCH"
fi
# Determine evaluation instances based on trigger type
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
if [[ "${{ github.event.label.name }}" == "run-eval-1" ]]; then
EVAL_INSTANCES="1"
elif [[ "${{ github.event.label.name }}" == "run-eval-2" ]]; then
EVAL_INSTANCES="2"
elif [[ "${{ github.event.label.name }}" == "run-eval-50" ]]; then
EVAL_INSTANCES="50"
elif [[ "${{ github.event.label.name }}" == "run-eval-100" ]]; then
EVAL_INSTANCES="100"
fi
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
EVAL_INSTANCES="${{ github.event.inputs.eval_instances }}"
else
# For release events, default to 50 instances
EVAL_INSTANCES="50"
fi
echo "Evaluation instances: $EVAL_INSTANCES"
echo "repo_url=$REPO_URL" >> $GITHUB_OUTPUT
echo "eval_branch=$EVAL_BRANCH" >> $GITHUB_OUTPUT
echo "eval_instances=$EVAL_INSTANCES" >> $GITHUB_OUTPUT
- name: Trigger remote job
run: |
# Determine PR number for the remote evaluation system
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
else
# For non-PR triggers, use the master issue number as PR number
PR_NUMBER="${{ env.MASTER_EVAL_ISSUE_NUMBER }}"
fi
curl -X POST \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"main\", \"inputs\": {\"github-repo\": \"${{ steps.eval_params.outputs.repo_url }}\", \"github-branch\": \"${{ steps.eval_params.outputs.eval_branch }}\", \"pr-number\": \"${PR_NUMBER}\", \"eval-instances\": \"${{ steps.eval_params.outputs.eval_instances }}\"}}" \
https://api.github.com/repos/All-Hands-AI/evaluation/actions/workflows/create-branch.yml/dispatches
# Send Slack message
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
TRIGGER_URL="https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
slack_text="PR $TRIGGER_URL has triggered evaluation on ${{ steps.eval_params.outputs.eval_instances }} instances..."
elif [[ "${{ github.event_name }}" == "release" ]]; then
TRIGGER_URL="https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}"
slack_text="Release $TRIGGER_URL has triggered evaluation on ${{ steps.eval_params.outputs.eval_instances }} instances..."
else
TRIGGER_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
slack_text="Manual trigger (${{ github.event.inputs.reason || 'No reason provided' }}) has triggered evaluation on ${{ steps.eval_params.outputs.eval_instances }} instances for branch ${{ steps.eval_params.outputs.eval_branch }}..."
fi
curl -X POST -H 'Content-type: application/json' --data '{"text":"'"$slack_text"'"}' \
https://hooks.slack.com/services/${{ secrets.SLACK_TOKEN }}
- name: Comment on issue/PR
uses: KeisukeYamashita/create-comment@v1
with:
# For PR triggers, comment on the PR. For other triggers, comment on the master issue
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || env.MASTER_EVAL_ISSUE_NUMBER }}
unique: false
comment: |
**Evaluation Triggered**
**Trigger:** ${{ github.event_name == 'pull_request' && format('Pull Request #{0}', github.event.pull_request.number) || (github.event_name == 'release' && 'Release') || format('Manual Trigger: {0}', github.event.inputs.reason || 'No reason provided') }}
**Branch:** ${{ steps.eval_params.outputs.eval_branch }}
**Instances:** ${{ steps.eval_params.outputs.eval_instances }}
**Commit:** ${{ github.sha }}
Running evaluation on the specified branch. Once eval is done, the results will be posted here.

View File

@@ -9,6 +9,7 @@ on:
jobs:
stale:
runs-on: blacksmith-4vcpu-ubuntu-2204
if: github.repository == 'OpenHands/OpenHands'
steps:
- uses: actions/stale@v9
with:

View File

@@ -1,156 +0,0 @@
# Workflow that validates the VSCode extension builds correctly
name: VSCode Extension CI
# * Always run on "main"
# * Run on PRs that have changes in the VSCode extension folder or this workflow
# * Run on tags that start with "ext-v"
on:
push:
branches:
- main
tags:
- 'ext-v*'
pull_request:
paths:
- 'openhands/integrations/vscode/**'
- 'build_vscode.py'
- '.github/workflows/vscode-extension-build.yml'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
# Validate VSCode extension builds correctly
validate-vscode-extension:
name: Validate VSCode Extension Build
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: useblacksmith/setup-node@v5
with:
node-version: '22'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install VSCode extension dependencies
working-directory: ./openhands/integrations/vscode
run: npm ci
- name: Build VSCode extension via build_vscode.py
run: python build_vscode.py
env:
# Ensure we don't skip the build
SKIP_VSCODE_BUILD: ""
- name: Validate .vsix file
run: |
# Verify the .vsix was created and is valid
if [ -f "openhands/integrations/vscode/openhands-vscode-0.0.1.vsix" ]; then
echo "✅ VSCode extension built successfully"
ls -la openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
# Basic validation that the .vsix is a valid zip file
echo "🔍 Validating .vsix structure..."
file openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
unzip -t openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
echo "✅ VSCode extension validation passed"
else
echo "❌ VSCode extension build failed - .vsix not found"
exit 1
fi
- name: Upload VSCode extension artifact
uses: actions/upload-artifact@v4
with:
name: vscode-extension
path: openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
retention-days: 7
- name: Comment on PR with artifact link
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Get file size for display
const vsixPath = 'openhands/integrations/vscode/openhands-vscode-0.0.1.vsix';
const stats = fs.statSync(vsixPath);
const fileSizeKB = Math.round(stats.size / 1024);
const comment = `## 🔧 VSCode Extension Built Successfully!
The VSCode extension has been built and is ready for testing.
**📦 Download**: [openhands-vscode-0.0.1.vsix](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (${fileSizeKB} KB)
**🚀 To install**:
1. Download the artifact from the workflow run above
2. In VSCode: \`Ctrl+Shift+P\` → "Extensions: Install from VSIX..."
3. Select the downloaded \`.vsix\` file
**✅ Tested with**: Node.js 22
**🔍 Validation**: File structure and integrity verified
---
*Built from commit ${{ github.sha }}*`;
// Check if we already commented on this PR and delete it
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('VSCode Extension Built Successfully')
);
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
}
// Create a new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
release:
name: Create GitHub Release
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: validate-vscode-extension
if: startsWith(github.ref, 'refs/tags/ext-v')
steps:
- name: Download .vsix artifact
uses: actions/download-artifact@v4
with:
name: vscode-extension
path: ./
- name: Create Release
uses: ncipollo/release-action@v1.16.0
with:
artifacts: "*.vsix"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
allowUpdates: true

View File

@@ -45,7 +45,7 @@ jobs:
"This issue has been labeled as **good first issue**, which means it's a great place to get started with the OpenHands project.\n\n" +
"If you're interested in working on it, feel free to! No need to ask for permission.\n\n" +
"Be sure to check out our [development setup guide](" + repoUrl + "/blob/main/Development.md) to get your environment set up, and follow our [contribution guidelines](" + repoUrl + "/blob/main/CONTRIBUTING.md) when you're ready to submit a fix.\n\n" +
"Feel free to join our developer community on [Slack](https://all-hands.dev/joinslack). You can ask for [help](https://openhands-ai.slack.com/archives/C078L0FUGUX), [feedback](https://openhands-ai.slack.com/archives/C086ARSNMGA), and even ask for a [PR review](https://openhands-ai.slack.com/archives/C08D8FJ5771).\n\n" +
"Feel free to join our developer community on [Slack](https://openhands.dev/joinslack). You can ask for [help](https://openhands-ai.slack.com/archives/C078L0FUGUX), [feedback](https://openhands-ai.slack.com/archives/C086ARSNMGA), and even ask for a [PR review](https://openhands-ai.slack.com/archives/C08D8FJ5771).\n\n" +
"🙌 Happy hacking! 🙌\n\n" +
"<!-- auto-comment:good-first-issue -->"
});

3
.gitignore vendored
View File

@@ -185,6 +185,9 @@ cython_debug/
.repomix
repomix-output.txt
# Emacs backup
*~
# evaluation
evaluation/evaluation_outputs
evaluation/outputs

View File

@@ -13,7 +13,6 @@ STAGED_FILES=$(git diff --cached --name-only)
# Check if any files match specific patterns
has_frontend_changes=false
has_backend_changes=false
has_vscode_changes=false
# Check each file individually to avoid issues with grep
for file in $STAGED_FILES; do
@@ -21,17 +20,12 @@ for file in $STAGED_FILES; do
has_frontend_changes=true
elif [[ $file == openhands/* || $file == evaluation/* || $file == tests/* ]]; then
has_backend_changes=true
# Check for VSCode extension changes (subset of backend changes)
if [[ $file == openhands/integrations/vscode/* ]]; then
has_vscode_changes=true
fi
fi
done
echo "Analyzing changes..."
echo "- Frontend changes: $has_frontend_changes"
echo "- Backend changes: $has_backend_changes"
echo "- VSCode extension changes: $has_vscode_changes"
# Run frontend linting if needed
if [ "$has_frontend_changes" = true ]; then
@@ -92,51 +86,6 @@ else
echo "Skipping backend checks (no backend changes detected)."
fi
# Run VSCode extension checks if needed
if [ "$has_vscode_changes" = true ]; then
# Check if we're in a CI environment
if [ -n "$CI" ]; then
echo "Skipping VSCode extension checks (CI environment detected)."
echo "WARNING: VSCode extension files have changed but checks are being skipped."
echo "Please run VSCode extension checks manually before submitting your PR."
else
echo "Running VSCode extension checks..."
if [ -d "openhands/integrations/vscode" ]; then
cd openhands/integrations/vscode || exit 1
echo "Running npm lint:fix..."
npm run lint:fix
if [ $? -ne 0 ]; then
echo "VSCode extension linting failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension linting passed!"
fi
echo "Running npm typecheck..."
npm run typecheck
if [ $? -ne 0 ]; then
echo "VSCode extension type checking failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension type checking passed!"
fi
echo "Running npm compile..."
npm run compile
if [ $? -ne 0 ]; then
echo "VSCode extension compilation failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension compilation passed!"
fi
cd ../../..
fi
fi
else
echo "Skipping VSCode extension checks (no VSCode extension changes detected)."
fi
# If no specific code changes detected, run basic checks
if [ "$has_frontend_changes" = false ] && [ "$has_backend_changes" = false ]; then

16
.vscode/settings.json vendored
View File

@@ -3,4 +3,20 @@
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"python.defaultInterpreterPath": "./.venv/bin/python",
"python.terminal.activateEnvironment": true,
"python.analysis.autoImportCompletions": true,
"python.analysis.autoSearchPaths": true,
"python.analysis.extraPaths": [
"./.venv/lib/python3.12/site-packages"
],
"python.analysis.packageIndexDepths": [
{
"name": "openhands",
"depth": 10,
"includeAllSymbols": true
}
],
"python.analysis.stubPath": "./.venv/lib/python3.12/site-packages",
}

View File

@@ -63,7 +63,7 @@ Frontend:
- We use TanStack Query (fka React Query) for data fetching and cache management
- Data Access Layer: API client methods are located in `frontend/src/api` and should never be called directly from UI components - they must always be wrapped with TanStack Query
- Custom hooks are located in `frontend/src/hooks/query/` and `frontend/src/hooks/mutation/`
- Query hooks should follow the pattern use[Resource] (e.g., `useConversationMicroagents`)
- 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
@@ -83,6 +83,116 @@ VSCode Extension:
- Use `vscode.window.createOutputChannel()` for debug logging instead of `showErrorMessage()` popups
- Pre-commit process runs both frontend and backend checks when committing extension changes
## Enterprise Directory
The `enterprise/` directory contains additional functionality that extends the open-source OpenHands codebase. This includes:
- Authentication and user management (Keycloak integration)
- Database migrations (Alembic)
- Integration services (GitHub, GitLab, Jira, Linear, Slack)
- Billing and subscription management (Stripe)
- Telemetry and analytics (PostHog, custom metrics framework)
### Enterprise Development Setup
**Prerequisites:**
- Python 3.12
- Poetry (for dependency management)
- Node.js 22.x (for frontend)
- Docker (optional)
**Setup Steps:**
1. First, build the main OpenHands project: `make build`
2. Then install enterprise dependencies: `cd enterprise && poetry install --with dev,test` (This can take a very long time. Be patient.)
3. Set up enterprise pre-commit hooks: `poetry run pre-commit install --config ./dev_config/python/.pre-commit-config.yaml`
**Running Enterprise Tests:**
```bash
# Enterprise unit tests (full suite)
PYTHONPATH=".:$PYTHONPATH" poetry run --project=enterprise pytest --forked -n auto -s -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./enterprise/tests/unit --cov=enterprise --cov-branch
# Test specific modules (faster for development)
cd enterprise
PYTHONPATH=".:$PYTHONPATH" poetry run pytest tests/unit/telemetry/ --confcutdir=tests/unit/telemetry
# Enterprise linting (IMPORTANT: use --show-diff-on-failure to match GitHub CI)
poetry run pre-commit run --all-files --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
```
**Running Enterprise Server:**
```bash
cd enterprise
make start-backend # Development mode with hot reload
# or
make run # Full application (backend + frontend)
```
**Key Configuration Files:**
- `enterprise/pyproject.toml` - Enterprise-specific dependencies
- `enterprise/Makefile` - Enterprise build and run commands
- `enterprise/dev_config/python/` - Linting and type checking configuration
- `enterprise/migrations/` - Database migration files
**Database Migrations:**
Enterprise uses Alembic for database migrations. When making schema changes:
1. Create migration files in `enterprise/migrations/versions/`
2. Test migrations thoroughly
3. The CI will check for migration conflicts on PRs
**Integration Development:**
The enterprise codebase includes integrations for:
- **GitHub** - PR management, webhooks, app installations
- **GitLab** - Similar to GitHub but for GitLab instances
- **Jira** - Issue tracking and project management
- **Linear** - Modern issue tracking
- **Slack** - Team communication and notifications
Each integration follows a consistent pattern with service classes, storage models, and API endpoints.
**Important Notes:**
- Enterprise code is licensed under Polyform Free Trial License (30-day limit)
- The enterprise server extends the OpenHands server through dynamic imports
- 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
**Enterprise Testing Best Practices:**
**Database Testing:**
- Use SQLite in-memory databases (`sqlite:///:memory:`) for unit tests instead of real PostgreSQL
- Create module-specific `conftest.py` files with database fixtures
- Mock external database connections in unit tests to avoid dependency on running services
- Use real database connections only for integration tests
**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`
- This ensures code works in both OpenHands and enterprise contexts
**Test Structure:**
- Place tests in `enterprise/tests/unit/` following the same structure as the source code
- Use `--confcutdir=tests/unit/[module]` when testing specific modules
- Create comprehensive fixtures for complex objects (databases, external services)
- Write platform-agnostic tests (avoid hardcoded OS-specific assertions)
**Mocking Strategy:**
- Use `AsyncMock` for async operations and `MagicMock` for complex objects
- Mock all external dependencies (databases, APIs, file systems) in unit tests
- Use `patch` with correct import paths (e.g., `telemetry.registry.logger` not `enterprise.telemetry.registry.logger`)
- Test both success and failure scenarios with proper error handling
**Coverage Goals:**
- Aim for 90%+ test coverage on new enterprise modules
- Focus on critical business logic and error handling paths
- Use `--cov-report=term-missing` to identify uncovered lines
**Troubleshooting:**
- If tests fail, ensure all dependencies are installed: `poetry install --with dev,test`
- For database issues, check migration status and run migrations if needed
- For frontend issues, ensure the main OpenHands frontend is built: `make build`
- Check logs in the `logs/` directory for runtime issues
- If tests fail with import errors, verify `PYTHONPATH=".:$PYTHONPATH"` is set
- **If GitHub CI fails but local linting passes**: Always use `--show-diff-on-failure` flag to match CI behavior exactly
## Template for Github Pull Request
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.

1
CNAME Normal file
View File

@@ -0,0 +1 @@
docs.all-hands.dev

View File

@@ -61,7 +61,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@all-hands.dev.
contact@openhands.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@@ -115,7 +115,9 @@ community.
### Slack Etiquettes
These Slack etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Lets work together to build a supportive and welcoming community!
These Slack etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all
community members. By following these best practices, we ensure effective communication and collaboration while
minimizing disruptions. Lets work together to build a supportive and welcoming community!
- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be difficult to interpret in text.
- Use threads for specific discussions to keep channels organized and easier to follow.
@@ -124,8 +126,11 @@ These Slack etiquette guidelines are designed to foster an inclusive, respectful
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions.
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to provide context.
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the matter is sensitive or private.
- Always adhere to [our standards](https://github.com/All-Hands-AI/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to alert you only when “LLMs” appears in messages.
- Always adhere to [our standards](https://github.com/OpenHands/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged.
For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned.
For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to
alert you only when “LLMs” appears in messages.
## Attribution

View File

@@ -1,43 +1,58 @@
# 🙌 The OpenHands Community
# The OpenHands Community
The OpenHands community is built around the belief that (1) AI and AI agents are going to fundamentally change the way
we build software, and (2) 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.
OpenHands is a community of engineers, academics, and enthusiasts reimagining software development for an AI-powered
world.
If this resonates with you, we'd love to have you join us in our quest!
## Mission
## 🤝 How to Join
Its very clear that AI is changing software development. We want the developer community to drive that change
organically, through open source.
Check out our [How to Join the Community section.](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-join-the-community)
So were not just building friendly interfaces for AI-driven development. Were publishing _building blocks_ that
empower developers to create new experiences, tailored to your own habits, needs, and imagination.
## 💪 Becoming a Contributor
## Ethos
We welcome contributions from everyone! Whether you're a developer, a researcher, or simply enthusiastic about advancing
the field of software engineering with AI, there are many ways to get involved:
We have two core values: **high openness** and **high agency**. While we dont expect everyone in the community to
embody these values, we want to establish them as norms.
- **Code Contributions:** Help us develop new core functionality, improve our agents, improve the frontend and other
interfaces, or anything else that would help make OpenHands better.
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in
evaluating the models, or suggest improvements.
- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability.
### High Openness
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
We welcome anyone and everyone into our community by default. You dont have to be a software developer to help us
build. You dont have to be pro-AI to help us learn.
## Code of Conduct
Our plans, our work, our successes, and our failures are all public record. We want the world to see not just the
fruits of our work, but the whole process of growing it.
We have a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect all contributors to adhere to.
Long story short, we are aiming for an open, welcoming, diverse, inclusive, and healthy community.
All contributors are expected to contribute to building this sort of community.
We welcome thoughtful criticism, whether its a comment on a PR or feedback on the community as a whole.
## 🛠️ Becoming a Maintainer
### High Agency
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:
Everyone should feel empowered to contribute to OpenHands. Whether its by making a PR, hosting an event, sharing
feedback, or just asking a question, dont hold back!
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.
OpenHands gives everyone the building blocks to create state-of-the-art developer experiences. We experiment constantly
and love building new things.
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).
Coding, development practices, and communities are changing rapidly. We wont hesitate to change direction and make big bets.
## Relationship to All Hands
OpenHands is supported by the for-profit organization [All Hands AI, Inc](https://www.openhands.dev/).
All Hands was founded by three of the first major contributors to OpenHands:
- Xingyao Wang, a UIUC PhD candidate who got OpenHands to the top of the SWE-bench leaderboards
- Graham Neubig, a CMU Professor who rallied the academic community around OpenHands
- Robert Brennan, a software engineer who architected the user-facing features of OpenHands
All Hands is an important part of the OpenHands ecosystem. Weve raised over $20M--mainly to hire developers and
researchers who can work on OpenHands full-time, and to provide them with expensive infrastructure. ([Join us!](https://allhandsai.applytojob.com/apply/))
But we see OpenHands as much larger, and ultimately more important, than All Hands. When our financial responsibility
to investors is at odds with our social responsibility to the community—as it inevitably will be, from time to time—we
promise to navigate that conflict thoughtfully and transparently.
At some point, we may transfer custody of OpenHands to an open source foundation. But for now,
the [Benevolent Dictator approach](http://www.catb.org/~esr/writings/cathedral-bazaar/homesteading/ar01s16.html) helps us move forward with speed and intention. If we ever forget the
“benevolent” part, please: fork us.

View File

@@ -6,36 +6,41 @@ Thanks for your interest in contributing to OpenHands! We welcome and appreciate
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)
For benchmarks and evaluation, see the [OpenHands/benchmarks](https://github.com/OpenHands/benchmarks) repository.
## Setting up Your Development Environment
We have a separate doc [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) that tells you how to set up a development workflow.
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.
## How Can I Contribute?
There are many ways that you can contribute:
1. **Download and use** OpenHands, and send [issues](https://github.com/All-Hands-AI/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.all-hands.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/All-Hands-AI/OpenHands/labels/good%20first%20issue) that may be ones to start on.
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.
## What Can I Build?
Here are a few ways you can help improve the codebase.
#### UI/UX
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.
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 #eng-ui-ux channel in our Slack
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.
#### Improving the agent
Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub/codeact_agent).
Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/OpenHands/OpenHands/tree/main/openhands/agenthub/codeact_agent).
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
@@ -46,19 +51,24 @@ We use the [SWE-bench](https://www.swebench.com/) benchmark to test our agent. Y
channel in Slack to learn more.
#### Adding a new agent
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.
#### Adding a new runtime
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.
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/All-Hands-AI/OpenHands/blob/main/openhands/runtime/base.py).
by implementing the [interface specified here](https://github.com/OpenHands/OpenHands/blob/main/openhands/runtime/base.py).
#### Testing
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 two kinds of tests: [`unit`](./tests/unit) and [`integration`](./evaluation/integration_tests). Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
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.
## Sending Pull Requests to OpenHands
@@ -66,7 +76,8 @@ 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
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:
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:
- `feat`: A new feature
- `fix`: A bug fix
@@ -84,9 +95,10 @@ 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/All-Hands-AI/OpenHands/pulls).
You may also check out previous PRs in the [PR list](https://github.com/OpenHands/OpenHands/pulls).
### Pull Request description
- 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.
@@ -97,7 +109,9 @@ please include a short message that we can add to our changelog.
### Opening Issues
If you notice any bugs or have any feature requests please open them via the [issues page](https://github.com/All-Hands-AI/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.
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.
Further, if you see an issue you like, please leave a "thumbs-up" or a comment, which will help us prioritize.
@@ -108,11 +122,13 @@ We're generally happy to consider all pull requests with the evaluation process
#### 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.
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:
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**

View File

@@ -2,11 +2,13 @@
## Contributors
We would like to thank all the [contributors](https://github.com/All-Hands-AI/OpenHands/graphs/contributors) who have helped make OpenHands possible. We greatly appreciate your dedication and hard work.
We would like to thank all the [contributors](https://github.com/OpenHands/OpenHands/graphs/contributors) who have
helped make OpenHands possible. We greatly appreciate your dedication and hard work.
## Open Source Projects
OpenHands includes and adapts the following open source projects. We are grateful for their contributions to the open source community:
OpenHands includes and adapts the following open source projects. We are grateful for their contributions to the
open source community:
#### [SWE Agent](https://github.com/princeton-nlp/swe-agent)
- License: MIT License
@@ -14,14 +16,14 @@ OpenHands includes and adapts the following open source projects. We are gratefu
#### [Aider](https://github.com/paul-gauthier/aider)
- License: Apache License 2.0
- Description: AI pair programming tool. OpenHands has adapted and integrated its linter module for code-related tasks in [`agentskills utilities`](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/runtime/plugins/agent_skills/utils/aider)
- Description: AI pair programming tool. OpenHands has adapted and integrated its linter module for code-related tasks in [`agentskills utilities`](https://github.com/OpenHands/OpenHands/tree/main/openhands/runtime/plugins/agent_skills/utils/aider)
#### [BrowserGym](https://github.com/ServiceNow/BrowserGym)
- License: Apache License 2.0
- Description: Adapted in implementing the browsing agent
### Reference Implementations for Evaluation Benchmarks
OpenHands integrates code of the reference implementations for the following agent evaluation benchmarks:
#### [HumanEval](https://github.com/openai/human-eval)
@@ -52,28 +54,44 @@ OpenHands integrates code of the reference implementations for the following age
#### [ProntoQA](https://github.com/asaparov/prontoqa)
- License: Apache License 2.0
## Open Source licenses
### MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### BSD 3-Clause License
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### Apache License 2.0
@@ -268,8 +286,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
Copyright [yyyy] [name of copyright owner]
### Non-Open Source Reference Implementations:
#### [MultiPL-E](https://github.com/nuprl/MultiPL-E)

View File

@@ -2,7 +2,7 @@
This guide is for people working on OpenHands and editing the source code.
If you wish to contribute your changes, check out the
[CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md)
[CONTRIBUTING.md](https://github.com/OpenHands/OpenHands/blob/main/CONTRIBUTING.md)
on how to clone and setup the project initially before moving on. Otherwise,
you can clone the OpenHands project directly.
@@ -76,7 +76,7 @@ variables in your terminal. The final configurations are set from highest to low
Environment variables > config.toml variables > default variables
**Note on Alternative Models:**
See [our documentation](https://docs.all-hands.dev/usage/llms) for recommended models.
See [our documentation](https://docs.openhands.dev/usage/llms) for recommended models.
### 4. Running the application
@@ -91,14 +91,14 @@ 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.
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.
components or interface enhancements.
```bash
make start-frontend
```
@@ -110,6 +110,7 @@ You can use OpenHands to develop and improve OpenHands itself! This is a powerfu
#### Quick Start
1. **Build and run OpenHands:**
```bash
export INSTALL_DOCKER=0
export RUNTIME=local
@@ -117,6 +118,7 @@ You can use OpenHands to develop and improve OpenHands itself! This is a powerfu
```
2. **Access the interface:**
- Local development: http://localhost:3001
- Remote/cloud environments: Use the appropriate external URL
@@ -159,7 +161,7 @@ poetry run pytest ./tests/unit/test_*.py
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/all-hands-ai/runtime:0.59-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:1.2-nikolaik`
## Develop inside Docker container
@@ -193,12 +195,12 @@ 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
- [/docs/DOC_STYLE_GUIDE.md](./docs/DOC_STYLE_GUIDE.md): Standards for writing and maintaining project documentation
- [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
- [/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
- [/microagents/README.md](./microagents/README.md): Information about the microagents architecture and implementation
- [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

@@ -3,14 +3,14 @@ These are the procedures and guidelines on how issues are triaged in this repo b
## General
* All issues must be tagged with **enhancement**, **bug** or **troubleshooting/help**.
* Issues may be tagged with what it relates to (**agent quality**, **resolver**, **CLI**, etc.).
* Issues may be tagged with what it relates to (**llm**, **app tab**, **UI/UX**, etc.).
## Severity
* **High**: High visibility issues or affecting many users.
* **Critical**: Affecting all users or potential security issues.
## Difficulty
* Issues with low implementation difficulty may be tagged with **good first issue**.
* Issues good for newcomers may be tagged with **good first issue**.
## Not Enough Information
* User is asked to provide more information (logs, how to reproduce, etc.) when the issue is not clear.
@@ -22,6 +22,6 @@ the issue may be closed as **not planned** (Usually after a week).
* Issues may be broken down into multiple issues if required.
## Stale and Auto Closures
* In order to keep a maintainable backlog, issues that have no activity within 30 days are automatically marked as **Stale**.
* If issues marked as **Stale** continue to have no activity for 7 more days, they will automatically be closed as not planned.
* In order to keep a maintainable backlog, issues that have no activity within 40 days are automatically marked as **Stale**.
* If issues marked as **Stale** continue to have no activity for 10 more days, they will automatically be closed as not planned.
* Issues may be reopened by maintainers if deemed important.

196
README.md
View File

@@ -1,178 +1,86 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: Code Less, Make More</h1>
<img src="https://raw.githubusercontent.com/OpenHands/docs/main/openhands/static/img/logo.png" alt="Logo" width="200">
<h1 align="center" style="border-bottom: none">OpenHands: AI-Driven Development</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<a href="https://github.com/OpenHands/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/badge/LICENSE-MIT-20B2AA?style=for-the-badge" alt="MIT License"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=811504672#gid=811504672"><img src="https://img.shields.io/badge/SWEBench-77.6-00cc00?logoColor=FFE165&style=for-the-badge" alt="Benchmark Score"></a>
<br/>
<a href="https://all-hands.dev/joinslack"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
<a href="https://docs.openhands.dev/sdk"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://arxiv.org/abs/2511.03690"><img src="https://img.shields.io/badge/Paper-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Tech Report"></a>
<!-- Keep these links. Translations will automatically update with the README. -->
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=de">Deutsch</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=es">Español</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=fr">français</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ja">日本語</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ko">한국어</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=pt">Português</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=ru">Русский</a> |
<a href="https://www.readme-i18n.com/All-Hands-AI/OpenHands?lang=zh">中文</a>
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=de">Deutsch</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=es">Español</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=fr">français</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=ja">日本語</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=ko">한국어</a> |
<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>
<hr>
</div>
Welcome to OpenHands (formerly OpenDevin), a platform for software development agents powered by AI.
<hr>
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
call APIs, and yes—even copy code snippets from StackOverflow.
🙌 Welcome to OpenHands, a [community](COMMUNITY.md) focused on AI-driven development. Wed love for you to [join us on Slack](https://dub.sh/openhands).
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for OpenHands Cloud](https://app.all-hands.dev) to get started.
There are a few ways to work with OpenHands:
> [!IMPORTANT]
> Using OpenHands for work? We'd love to chat! Fill out
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
### OpenHands Software Agent SDK
The SDK is a composable Python library that contains all of our agentic tech. It's the engine that powers everything else below.
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $20 in free credits for new users.
Define agents in code, then run them locally, or scale to 1000s of agents in the cloud.
## 💻 Running OpenHands Locally
[Check out the docs](https://docs.openhands.dev/sdk) or [view the source](https://github.com/OpenHands/software-agent-sdk/)
### Option 1: CLI Launcher (Recommended)
### OpenHands CLI
The CLI is the easiest way to start using OpenHands. The experience will be familiar to anyone who has worked
with e.g. Claude Code or Codex. You can power it with Claude, GPT, or any other LLM.
The easiest way to run OpenHands locally is using the CLI launcher with [uv](https://docs.astral.sh/uv/). This provides better isolation from your current project's virtual environment and is required for OpenHands' default MCP servers.
[Check out the docs](https://docs.openhands.dev/openhands/usage/run-openhands/cli-mode) or [view the source](https://github.com/OpenHands/OpenHands-CLI)
**Install uv** (if you haven't already):
### OpenHands Local GUI
Use the Local GUI for running agents on your laptop. It comes with a REST API and a single-page React application.
The experience will be familiar to anyone who has used Devin or Jules.
See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for the latest installation instructions for your platform.
[Check out the docs](https://docs.openhands.dev/openhands/usage/run-openhands/local-setup) or view the source in this repo.
**Launch OpenHands**:
```bash
# Launch the GUI server
uvx --python 3.12 --from openhands-ai openhands serve
### OpenHands Cloud
This is a deployment of OpenHands GUI, running on hosted infrastructure.
# Or launch the CLI
uvx --python 3.12 --from openhands-ai openhands
```
You can try it with a free $10 credit by [signing in with your GitHub or GitLab account](https://app.all-hands.dev).
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000) (for GUI mode)!
OpenHands Cloud comes with source-available features and integrations:
- Integrations with Slack, Jira, and Linear
- Multi-user support
- RBAC and permissions
- Collaboration features (e.g., conversation sharing)
### Option 2: Docker
### OpenHands Enterprise
Large enterprises can work with us to self-host OpenHands Cloud in their own VPC, via Kubernetes.
OpenHands Enterprise can also work with the CLI and SDK above.
<details>
<summary>Click to expand Docker command</summary>
OpenHands Enterprise is source-available--you can see all the source code here in the enterprise/ directory,
but you'll need to purchase a license if you want to run it for more than one month.
You can also run OpenHands directly with Docker:
Enterprise contracts also come with extended support and access to our research team.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik
Learn more at [openhands.dev/enterprise](https://openhands.dev/enterprise)
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.59
```
### Everything Else
</details>
Check out our [Product Roadmap](https://github.com/orgs/openhands/projects/1), and feel free to
[open up an issue](https://github.com/OpenHands/OpenHands/issues) if there's something you'd like to see!
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
You might also be interested in our [evaluation infrastructure](https://github.com/OpenHands/benchmarks), our [chrome extension](https://github.com/OpenHands/openhands-chrome-extension/), or our [Theory-of-Mind module](https://github.com/OpenHands/ToM-SWE).
> [!WARNING]
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
> to secure your deployment by restricting network binding and implementing additional security measures.
All our work is available under the MIT license, except for the `enterprise/` directory in this repository (see the [enterprise license](enterprise/LICENSE) for details).
The core `openhands` and `agent-server` Docker images are fully MIT-licensed as well.
### Getting Started
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude Sonnet 4.5](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-5-20250929`)
works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
system requirements and more information.
## 💡 Other ways to run OpenHands
> [!WARNING]
> OpenHands is meant to be run by a single user on their local workstation.
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
>
> If you're interested in running OpenHands in a multi-tenant environment, check out the source-available, commercially-licensed
> [OpenHands Cloud Helm Chart](https://github.com/all-Hands-AI/OpenHands-cloud)
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
interact with it via a [friendly CLI](https://docs.all-hands.dev/usage/how-to/cli-mode),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/usage/how-to/github-action).
Visit [Running OpenHands](https://docs.all-hands.dev/usage/installation) for more information and setup instructions.
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/usage/troubleshooting) can help.
## 📖 Documentation
To learn more about the project, and for tips on using OpenHands,
check out our [documentation](https://docs.all-hands.dev/usage/getting-started).
There you'll find resources on how to use different LLM providers,
troubleshooting resources, and advanced configuration options.
## 🤝 How to Join the Community
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Github:
- [Join our Slack workspace](https://all-hands.dev/joinslack) - Here we talk about research, architecture, and future development.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md).
## 📈 Progress
See the monthly OpenHands roadmap [here](https://github.com/orgs/All-Hands-AI/projects/1) (updated at the maintainer's meeting at the end of each month).
<p align="center">
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
</a>
</p>
## 📜 License
Distributed under the MIT License, with the exception of the `enterprise/` folder. See [`LICENSE`](./LICENSE) for more information.
## 🙏 Acknowledgements
OpenHands is built by a large number of contributors, and every contribution is greatly appreciated! We also build upon other open source projects, and we are deeply thankful for their work.
For a list of open source projects and licenses used in OpenHands, please see our [CREDITS.md](./CREDITS.md) file.
## 📚 Cite
```
@inproceedings{
wang2025openhands,
title={OpenHands: An Open Platform for {AI} Software Developers as Generalist Agents},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
booktitle={The Thirteenth International Conference on Learning Representations},
year={2025},
url={https://openreview.net/forum?id=OJd3ayDDoF}
}
```
If you need help with anything, or just want to chat, [come find us on Slack](https://dub.sh/openhands).

View File

@@ -1,113 +0,0 @@
import os
import pathlib
import subprocess
# This script is intended to be run by Poetry during the build process.
# Define the expected name of the .vsix file based on the extension's package.json
# This should match the name and version in openhands-vscode/package.json
EXTENSION_NAME = 'openhands-vscode'
EXTENSION_VERSION = '0.0.1'
VSIX_FILENAME = f'{EXTENSION_NAME}-{EXTENSION_VERSION}.vsix'
# Paths
ROOT_DIR = pathlib.Path(__file__).parent.resolve()
VSCODE_EXTENSION_DIR = ROOT_DIR / 'openhands' / 'integrations' / 'vscode'
def check_node_version():
"""Check if Node.js version is sufficient for building the extension."""
try:
result = subprocess.run(
['node', '--version'], capture_output=True, text=True, check=True
)
version_str = result.stdout.strip()
# Extract major version number (e.g., "v12.22.9" -> 12)
major_version = int(version_str.lstrip('v').split('.')[0])
return major_version >= 18 # Align with frontend actual usage (18.20.1)
except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
return False
def build_vscode_extension():
"""Builds the VS Code extension."""
vsix_path = VSCODE_EXTENSION_DIR / VSIX_FILENAME
# Check if VSCode extension build is disabled via environment variable
if os.environ.get('SKIP_VSCODE_BUILD', '').lower() in ('1', 'true', 'yes'):
print('--- Skipping VS Code extension build (SKIP_VSCODE_BUILD is set) ---')
if vsix_path.exists():
print(f'--- Using existing VS Code extension: {vsix_path} ---')
else:
print('--- No pre-built VS Code extension found ---')
return
# Check Node.js version - if insufficient, use pre-built extension as fallback
if not check_node_version():
print('--- Warning: Node.js version < 18 detected or Node.js not found ---')
print('--- Skipping VS Code extension build (requires Node.js >= 18) ---')
print('--- Using pre-built extension if available ---')
if not vsix_path.exists():
print('--- Warning: No pre-built VS Code extension found ---')
print('--- VS Code extension will not be available ---')
else:
print(f'--- Using pre-built VS Code extension: {vsix_path} ---')
return
print(f'--- Building VS Code extension in {VSCODE_EXTENSION_DIR} ---')
try:
# Ensure npm dependencies are installed
print('--- Running npm install for VS Code extension ---')
subprocess.run(
['npm', 'install'],
cwd=VSCODE_EXTENSION_DIR,
check=True,
shell=os.name == 'nt',
)
# Package the extension
print(f'--- Packaging VS Code extension ({VSIX_FILENAME}) ---')
subprocess.run(
['npm', 'run', 'package-vsix'],
cwd=VSCODE_EXTENSION_DIR,
check=True,
shell=os.name == 'nt',
)
# Verify the generated .vsix file exists
if not vsix_path.exists():
raise FileNotFoundError(
f'VS Code extension package not found after build: {vsix_path}'
)
print(f'--- VS Code extension built successfully: {vsix_path} ---')
except subprocess.CalledProcessError as e:
print(f'--- Warning: Failed to build VS Code extension: {e} ---')
print('--- Continuing without building extension ---')
if not vsix_path.exists():
print('--- Warning: No pre-built VS Code extension found ---')
print('--- VS Code extension will not be available ---')
def build(setup_kwargs):
"""This function is called by Poetry during the build process.
`setup_kwargs` is a dictionary that will be passed to `setuptools.setup()`.
"""
print('--- Running custom Poetry build script (build_vscode.py) ---')
# Build the VS Code extension and place the .vsix file
build_vscode_extension()
# Poetry will handle including files based on pyproject.toml `include` patterns.
# Ensure openhands/integrations/vscode/*.vsix is included there.
print('--- Custom Poetry build script (build_vscode.py) finished ---')
if __name__ == '__main__':
print('Running build_vscode.py directly for testing VS Code extension packaging...')
build_vscode_extension()
print('Direct execution of build_vscode.py finished.')

View File

@@ -189,7 +189,7 @@ model = "gpt-4o"
# Whether to use native tool calling if supported by the model. Can be true, false, or None by default, which chooses the model's default behavior based on the evaluation.
# ATTENTION: Based on evaluation, enabling native function calling may lead to worse results
# in some scenarios. Use with caution and consider testing with your specific use case.
# https://github.com/All-Hands-AI/OpenHands/pull/4711
# https://github.com/OpenHands/OpenHands/pull/4711
#native_tool_calling = None
@@ -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:24.8-trixie-slim AS frontend-builder
FROM node:25.2-trixie-slim AS frontend-builder
WORKDIR /app
@@ -73,7 +73,7 @@ ENV VIRTUAL_ENV=/app/.venv \
COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --chown=openhands:openhands --chmod=770 ./microagents ./microagents
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 ./

View File

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

View File

@@ -104,6 +104,9 @@ RUN apt-get update && apt-get install -y \
&& apt-get clean \
&& apt-get autoremove -y
# mark /app as safe git directory to avoid pre-commit errors
RUN git config --system --add safe.directory /app
WORKDIR /app
# cache build dependencies

View File

@@ -1,7 +1,7 @@
# Develop in Docker
> [!WARNING]
> This is not officially supported and may not work.
> This way of running OpenHands is not officially supported. It is maintained by the community and may not work.
Install [Docker](https://docs.docker.com/engine/install/) on your host machine and run:

View File

@@ -12,7 +12,8 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.59-nikolaik}
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/runtime}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-1.2-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -6,7 +6,7 @@ that depends on the `base_image` **AND** a [Python source distribution](https://
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 openhands/runtime/utils/runtime_build.py \
poetry run python3 -m openhands.runtime.utils.runtime_build \
--base_image nikolaik/python-nodejs:python3.12-nodejs22 \
--build_folder containers/runtime
```

View File

@@ -1,5 +1,5 @@
DOCKER_REGISTRY=ghcr.io
DOCKER_ORG=all-hands-ai
DOCKER_ORG=openhands
DOCKER_BASE_DIR="./containers/runtime"
DOCKER_IMAGE=runtime
# These variables will be appended by the runtime_build.py script

View File

@@ -3,13 +3,22 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
- id: end-of-file-fixer
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
- repo: local
hooks:
- id: warn-appmode-oss
name: "Warn on AppMode.OSS in backend (use AppMode.OPENHANDS)"
language: system
entry: bash -lc 'if rg -n "\\bAppMode\\.OSS\\b" openhands tests/unit; then echo "Found AppMode.OSS usage. Prefer AppMode.OPENHANDS."; exit 1; fi'
pass_filenames: false
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.1
hooks:
@@ -28,12 +37,12 @@ repos:
entry: ruff check --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
args: [--fix, --unsafe-fixes]
exclude: ^(third_party/|enterprise/|openhands-cli/)
exclude: ^(third_party/|enterprise/)
# Run the formatter.
- id: ruff-format
entry: ruff format --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
exclude: ^(third_party/|enterprise/|openhands-cli/)
exclude: ^(third_party/|enterprise/)
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0

View File

@@ -7,7 +7,8 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik}
- AGENT_SERVER_IMAGE_REPOSITORY=${AGENT_SERVER_IMAGE_REPOSITORY:-ghcr.io/openhands/agent-server}
- AGENT_SERVER_IMAGE_TAG=${AGENT_SERVER_IMAGE_TAG:-31536c8-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

@@ -1,5 +1,5 @@
ARG OPENHANDS_VERSION=latest
ARG BASE="ghcr.io/all-hands-ai/openhands"
ARG BASE="ghcr.io/openhands/openhands"
FROM ${BASE}:${OPENHANDS_VERSION}
# Datadog labels
@@ -24,16 +24,15 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*
# Install Python packages with security fixes
RUN 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 && \
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
pip install --upgrade \
/app/.venv/bin/pip install --upgrade \
"mcp>=1.10.0" \
"pillow>=11.3.0"
WORKDIR /app
COPY enterprise .
COPY --chown=openhands:openhands --chmod=770 enterprise .
RUN chown -R openhands:openhands /app && chmod -R 770 /app
USER openhands
# Command will be overridden by Kubernetes deployment template

View File

@@ -2,7 +2,7 @@ BACKEND_HOST ?= "127.0.0.1"
BACKEND_PORT = 3000
BACKEND_HOST_PORT = "$(BACKEND_HOST):$(BACKEND_PORT)"
FRONTEND_PORT = 3001
OPENHANDS_PATH ?= "../../OpenHands"
OPENHANDS_PATH ?= ".."
OPENHANDS := $(OPENHANDS_PATH)
OPENHANDS_FRONTEND_PATH = $(OPENHANDS)/frontend/build

View File

@@ -1,6 +1,6 @@
# OpenHands Enterprise Server
> [!WARNING]
> This software is licensed under the [Polyform Free Trial License](./LICENSE). This is **NOT** an open source license. Usage is limited to 30 days per calendar year without a commercial license. If you would like to use it beyond 30 days, please [contact us](https://www.all-hands.dev/contact).
> This software is licensed under the [Polyform Free Trial License](./LICENSE). This is **NOT** an open source license. Usage is limited to 30 days per calendar year without a commercial license. If you would like to use it beyond 30 days, please [contact us](https://www.openhands.dev/contact).
> [!WARNING]
> This is a work in progress and may contain bugs, incomplete features, or breaking changes.
@@ -8,15 +8,15 @@
This directory contains the enterprise server used by [OpenHands Cloud](https://github.com/All-Hands-AI/OpenHands-Cloud/). The official, public version of OpenHands Cloud is available at
[app.all-hands.dev](https://app.all-hands.dev).
You may also want to check out the MIT-licensed [OpenHands](https://github.com/All-Hands-AI/OpenHands)
You may also want to check out the MIT-licensed [OpenHands](https://github.com/OpenHands/OpenHands)
## Extension of OpenHands (OSS)
## Extension of OpenHands
The code in `/enterprise` directory builds on top of open source (OSS) code, extending its functionality. The enterprise code is entangled with the OSS code in two ways
The code in `/enterprise` builds on top of OpenHands (MIT-licensed), extending its functionality. The enterprise code is entangled with OpenHands in two ways:
- Enterprise stacks on top of OSS. For example, the middleware in enterprise is stacked right on top of the middlewares in OSS. In `SAAS`, the middleware from BOTH repos will be present and running (which can sometimes cause conflicts)
- Enterprise stacks on top of OpenHands. For example, the middleware in enterprise is stacked right on top of the middlewares in OpenHands. In `SAAS`, the middleware from BOTH repos will be present and running (which can sometimes cause conflicts)
- Enterprise overrides the implementation in OSS (only one is present at a time). For example, the server config SaasServerConfig which overrides [`ServerConfig`](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/server/config/server_config.py#L8) on OSS. This is done through dynamic imports ([see here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/server/config/server_config.py#L37-#L45))
- Enterprise overrides the implementation in OpenHands (only one is present at a time). For example, the server config SaasServerConfig overrides [`ServerConfig`](https://github.com/OpenHands/OpenHands/blob/main/openhands/server/config/server_config.py#L8) in OpenHands. This is done through dynamic imports ([see here](https://github.com/OpenHands/OpenHands/blob/main/openhands/server/config/server_config.py#L37-#L45))
Key areas that change on `SAAS` are
@@ -26,11 +26,11 @@ Key areas that change on `SAAS` are
### Authentication
| Aspect | OSS | Enterprise |
| Aspect | OpenHands | Enterprise |
| ------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Authentication Method** | User adds a personal access token (PAT) through the UI | User performs OAuth through the UI. The Github app provides a short-lived access token and refresh token |
| **Authentication Method** | User adds a personal access token (PAT) through the UI | User performs OAuth through the UI. The GitHub app provides a short-lived access token and refresh token |
| **Token Storage** | PAT is stored in **Settings** | Token is stored in **GithubTokenManager** (a file store in our backend) |
| **Authenticated status** | We simply check if token exists in `Settings` | We issue a signed cookie with `github_user_id` during oauth, so subsequent requests with the cookie can be considered authenticated |
| **Authenticated status** | We simply check if token exists in `Settings` | We issue a signed cookie with `github_user_id` during OAuth, so subsequent requests with the cookie can be considered authenticated |
Note that in the future, authentication will happen via keycloak. All modifications for authentication will happen in enterprise.
@@ -38,7 +38,7 @@ Note that in the future, authentication will happen via keycloak. All modificati
The github service is responsible for interacting with Github APIs. As a consequence, it uses the user's token and refreshes it if need be
| Aspect | OSS | Enterprise |
| Aspect | OpenHands | Enterprise |
| ------------------------- | -------------------------------------- | ---------------------------------------------- |
| **Class used** | `GitHubService` | `SaaSGitHubService` |
| **Token used** | User's PAT fetched from `Settings` | User's token fetched from `GitHubTokenManager` |
@@ -50,7 +50,7 @@ NOTE: in the future we will simply replace the `GithubTokenManager` with keycloa
## User ID vs User Token
- On OSS, the entire APP revolves around the Github token the user sets. `openhands/server` uses `request.state.github_token` for the entire app
- 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)
Note that introducing Github User ID on OSS, for instance, will cause large breakages.
Note that introducing GitHub User ID in OpenHands, for instance, will cause large breakages.

View File

@@ -721,6 +721,7 @@
"https://$WEB_HOST/oauth/keycloak/callback",
"https://$WEB_HOST/oauth/keycloak/offline/callback",
"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/*"
],

View File

@@ -0,0 +1,856 @@
# OpenHands Enterprise Usage Telemetry Service
## Table of Contents
1. [Introduction](#1-introduction)
- 1.1 [Problem Statement](#11-problem-statement)
- 1.2 [Proposed Solution](#12-proposed-solution)
2. [User Interface](#2-user-interface)
- 2.1 [License Warning Banner](#21-license-warning-banner)
- 2.2 [Administrator Experience](#22-administrator-experience)
3. [Other Context](#3-other-context)
- 3.1 [Replicated Platform Integration](#31-replicated-platform-integration)
- 3.2 [Administrator Email Detection Strategy](#32-administrator-email-detection-strategy)
- 3.3 [Metrics Collection Framework](#33-metrics-collection-framework)
4. [Technical Design](#4-technical-design)
- 4.1 [Database Schema](#41-database-schema)
- 4.1.1 [Telemetry Metrics Table](#411-telemetry-metrics-table)
- 4.1.2 [Telemetry Identity Table](#412-telemetry-identity-table)
- 4.2 [Metrics Collection Framework](#42-metrics-collection-framework)
- 4.2.1 [Base Collector Interface](#421-base-collector-interface)
- 4.2.2 [Collector Registry](#422-collector-registry)
- 4.2.3 [Example Collector Implementation](#423-example-collector-implementation)
- 4.3 [Collection and Upload System](#43-collection-and-upload-system)
- 4.3.1 [Metrics Collection Processor](#431-metrics-collection-processor)
- 4.3.2 [Replicated Upload Processor](#432-replicated-upload-processor)
- 4.4 [License Warning System](#44-license-warning-system)
- 4.4.1 [License Status Endpoint](#441-license-status-endpoint)
- 4.4.2 [UI Integration](#442-ui-integration)
- 4.5 [Cronjob Configuration](#45-cronjob-configuration)
- 4.5.1 [Collection Cronjob](#451-collection-cronjob)
- 4.5.2 [Upload Cronjob](#452-upload-cronjob)
5. [Implementation Plan](#5-implementation-plan)
- 5.1 [Database Schema and Models (M1)](#51-database-schema-and-models-m1)
- 5.1.1 [OpenHands - Database Migration](#511-openhands---database-migration)
- 5.1.2 [OpenHands - Model Tests](#512-openhands---model-tests)
- 5.2 [Metrics Collection Framework (M2)](#52-metrics-collection-framework-m2)
- 5.2.1 [OpenHands - Core Collection Framework](#521-openhands---core-collection-framework)
- 5.2.2 [OpenHands - Example Collectors](#522-openhands---example-collectors)
- 5.2.3 [OpenHands - Framework Tests](#523-openhands---framework-tests)
- 5.3 [Collection and Upload Processors (M3)](#53-collection-and-upload-processors-m3)
- 5.3.1 [OpenHands - Collection Processor](#531-openhands---collection-processor)
- 5.3.2 [OpenHands - Upload Processor](#532-openhands---upload-processor)
- 5.3.3 [OpenHands - Integration Tests](#533-openhands---integration-tests)
- 5.4 [License Warning API (M4)](#54-license-warning-api-m4)
- 5.4.1 [OpenHands - License Status API](#541-openhands---license-status-api)
- 5.4.2 [OpenHands - API Integration](#542-openhands---api-integration)
- 5.5 [UI Warning Banner (M5)](#55-ui-warning-banner-m5)
- 5.5.1 [OpenHands - UI Warning Banner](#551-openhands---ui-warning-banner)
- 5.5.2 [OpenHands - UI Integration](#552-openhands---ui-integration)
- 5.6 [Helm Chart Deployment Configuration (M6)](#56-helm-chart-deployment-configuration-m6)
- 5.6.1 [OpenHands-Cloud - Cronjob Manifests](#561-openhands-cloud---cronjob-manifests)
- 5.6.2 [OpenHands-Cloud - Configuration Management](#562-openhands-cloud---configuration-management)
- 5.7 [Documentation and Enhanced Collectors (M7)](#57-documentation-and-enhanced-collectors-m7)
- 5.7.1 [OpenHands - Advanced Collectors](#571-openhands---advanced-collectors)
- 5.7.2 [OpenHands - Monitoring and Testing](#572-openhands---monitoring-and-testing)
- 5.7.3 [OpenHands - Technical Documentation](#573-openhands---technical-documentation)
## 1. Introduction
### 1.1 Problem Statement
OpenHands Enterprise (OHE) helm charts are publicly available but not open source, creating a visibility gap for the sales team. Unknown users can install and use OHE without the vendor's knowledge, preventing proper customer engagement and sales pipeline management. Without usage telemetry, the vendor cannot identify potential customers, track installation health, or proactively support users who may need assistance.
### 1.2 Proposed Solution
We propose implementing a comprehensive telemetry service that leverages the Replicated metrics platform and Python SDK to track OHE installations and usage. The solution provides automatic customer discovery, instance monitoring, and usage metrics collection while maintaining a clear license compliance pathway.
The system consists of three main components: (1) a pluggable metrics collection framework that allows developers to easily define and register custom metrics collectors, (2) automated cronjobs that periodically collect metrics and upload them to Replicated's vendor portal, and (3) a license compliance warning system that displays UI notifications when telemetry uploads fail, indicating potential license expiration.
The design ensures that telemetry cannot be easily disabled without breaking core OHE functionality by tying the warning system to environment variables that are essential for OHE operation. This approach balances user transparency with business requirements for customer visibility.
## 2. User Interface
### 2.1 License Warning Banner
When telemetry uploads fail for more than 4 days, users will see a prominent warning banner in the OpenHands Enterprise UI:
```
⚠️ Your OpenHands Enterprise license will expire in 30 days. Please contact support if this issue persists.
```
The banner appears at the top of all pages and cannot be permanently dismissed while the condition persists. Users can temporarily dismiss it, but it will reappear on page refresh until telemetry uploads resume successfully.
### 2.2 Administrator Experience
System administrators will not need to configure the telemetry system manually. The service automatically:
1. **Detects OHE installations** using existing required environment variables (`GITHUB_APP_CLIENT_ID`, `KEYCLOAK_SERVER_URL`, etc.)
2. **Generates unique customer identifiers** using administrator contact information:
- Customer email: Determined by the following priority order:
1. `OPENHANDS_ADMIN_EMAIL` environment variable (if set in helm values)
2. Email of the first user who accepted Terms of Service (earliest `accepted_tos` timestamp)
- Instance ID: Automatically generated by Replicated SDK using machine fingerprinting (IOPlatformUUID on macOS, D-Bus machine ID on Linux, Machine GUID on Windows)
- **No Fallback**: If neither email source is available, telemetry collection is skipped until at least one user exists
3. **Collects and uploads metrics transparently** in the background via weekly collection and daily upload cronjobs
4. **Displays warnings only when necessary** for license compliance - no notifications appear during normal operation
## 3. Other Context
### 3.1 Replicated Platform Integration
The Replicated platform provides vendor-hosted infrastructure for collecting customer and instance telemetry. The Python SDK handles authentication, state management, and reliable metric delivery. Key concepts:
- **Customer**: Represents a unique OHE installation, identified by email or installation fingerprint
- **Instance**: Represents a specific deployment of OHE for a customer
- **Metrics**: Custom key-value data points collected from the installation
- **Status**: Instance health indicators (running, degraded, updating, etc.)
The SDK automatically handles machine fingerprinting, local state caching, and retry logic for failed uploads.
### 3.2 Administrator Email Detection Strategy
To identify the appropriate administrator contact for sales outreach, the system uses a three-tier approach that avoids performance penalties on user authentication:
**Tier 1: Explicit Configuration** - The `OPENHANDS_ADMIN_EMAIL` environment variable allows administrators to explicitly specify the contact email during deployment.
**Tier 2: First Active User Detection** - If no explicit email is configured, the system identifies the first user who accepted Terms of Service (earliest `accepted_tos` timestamp with a valid email). This represents the first person to actively engage with the system and is very likely the administrator or installer.
**No Fallback Needed** - If neither email source is available, telemetry collection is skipped entirely. This ensures we only report meaningful usage data when there are actual active users.
**Performance Optimization**: The admin email determination is performed only during telemetry upload attempts, ensuring zero performance impact on user login flows.
### 3.3 Metrics Collection Framework
The proposed collector framework allows developers to define metrics in a single file change:
```python
@register_collector("user_activity")
class UserActivityCollector(MetricsCollector):
def collect(self) -> Dict[str, Any]:
# Query database and return metrics
return {"active_users_7d": count, "conversations_created": total}
```
Collectors are automatically discovered and executed by the collection cronjob, making the system extensible without modifying core collection logic.
## 4. Technical Design
### 4.1 Database Schema
#### 4.1.1 Telemetry Metrics Table
Stores collected metrics with transmission status tracking:
```sql
CREATE TABLE telemetry_metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
collected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
metrics_data JSONB NOT NULL,
uploaded_at TIMESTAMP WITH TIME ZONE NULL,
upload_attempts INTEGER DEFAULT 0,
last_upload_error TEXT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_telemetry_metrics_collected_at ON telemetry_metrics(collected_at);
CREATE INDEX idx_telemetry_metrics_uploaded_at ON telemetry_metrics(uploaded_at);
```
#### 4.1.2 Telemetry Identity Table
Stores persistent identity information that must survive container restarts:
```sql
CREATE TABLE telemetry_identity (
id INTEGER PRIMARY KEY DEFAULT 1,
customer_id VARCHAR(255) NULL,
instance_id VARCHAR(255) NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT single_identity_row CHECK (id = 1)
);
```
**Design Rationale:**
- **Separation of Concerns**: Identity data (customer_id, instance_id) is separated from operational data
- **Persistent vs Computed**: Only data that cannot be reliably recomputed is persisted
- **Upload Tracking**: Upload timestamps are tied directly to the metrics they represent
- **Simplified Queries**: System state can be derived from metrics table (e.g., `MAX(uploaded_at)` for last successful upload)
### 4.2 Metrics Collection Framework
#### 4.2.1 Base Collector Interface
```python
from abc import ABC, abstractmethod
from typing import Dict, Any, List
from dataclasses import dataclass
@dataclass
class MetricResult:
key: str
value: Any
class MetricsCollector(ABC):
"""Base class for metrics collectors."""
@abstractmethod
def collect(self) -> List[MetricResult]:
"""Collect metrics and return results."""
pass
@property
@abstractmethod
def collector_name(self) -> str:
"""Unique name for this collector."""
pass
def should_collect(self) -> bool:
"""Override to add collection conditions."""
return True
```
#### 4.2.2 Collector Registry
```python
from typing import Dict, Type, List
import importlib
import pkgutil
class CollectorRegistry:
"""Registry for metrics collectors."""
def __init__(self):
self._collectors: Dict[str, Type[MetricsCollector]] = {}
def register(self, collector_class: Type[MetricsCollector]) -> None:
"""Register a collector class."""
collector = collector_class()
self._collectors[collector.collector_name] = collector_class
def get_all_collectors(self) -> List[MetricsCollector]:
"""Get instances of all registered collectors."""
return [cls() for cls in self._collectors.values()]
def discover_collectors(self, package_path: str) -> None:
"""Auto-discover collectors in a package."""
# Implementation to scan for @register_collector decorators
pass
# Global registry instance
collector_registry = CollectorRegistry()
def register_collector(name: str):
"""Decorator to register a collector."""
def decorator(cls: Type[MetricsCollector]) -> Type[MetricsCollector]:
collector_registry.register(cls)
return cls
return decorator
```
#### 4.2.3 Example Collector Implementation
```python
@register_collector("system_metrics")
class SystemMetricsCollector(MetricsCollector):
"""Collects basic system and usage metrics."""
@property
def collector_name(self) -> str:
return "system_metrics"
def collect(self) -> List[MetricResult]:
results = []
# Collect user count
with session_maker() as session:
user_count = session.query(UserSettings).count()
results.append(MetricResult(
key="total_users",
value=user_count
))
# 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()
results.append(MetricResult(
key="conversations_30d",
value=conversation_count
))
return results
```
### 4.3 Collection and Upload System
#### 4.3.1 Metrics Collection Processor
```python
class TelemetryCollectionProcessor(MaintenanceTaskProcessor):
"""Maintenance task processor for collecting metrics."""
collection_interval_days: int = 7
async def __call__(self, task: MaintenanceTask) -> dict:
"""Collect metrics from all registered collectors."""
# Check if collection is needed
if not self._should_collect():
return {"status": "skipped", "reason": "too_recent"}
# Collect metrics from all registered collectors
all_metrics = {}
collector_results = {}
for collector in collector_registry.get_all_collectors():
try:
if collector.should_collect():
results = collector.collect()
for result in results:
all_metrics[result.key] = result.value
collector_results[collector.collector_name] = len(results)
except Exception as e:
logger.error(f"Collector {collector.collector_name} failed: {e}")
collector_results[collector.collector_name] = f"error: {e}"
# Store metrics in database
with session_maker() as session:
telemetry_record = TelemetryMetrics(
metrics_data=all_metrics,
collected_at=datetime.now(timezone.utc)
)
session.add(telemetry_record)
session.commit()
# Note: No need to track last_collection_at separately
# Can be derived from MAX(collected_at) in telemetry_metrics
return {
"status": "completed",
"metrics_collected": len(all_metrics),
"collectors_run": collector_results
}
def _should_collect(self) -> bool:
"""Check if collection is needed based on interval."""
with session_maker() as session:
# Get last collection time from metrics table
last_collected = session.query(func.max(TelemetryMetrics.collected_at)).scalar()
if not last_collected:
return True
time_since_last = datetime.now(timezone.utc) - last_collected
return time_since_last.days >= self.collection_interval_days
```
#### 4.3.2 Replicated Upload Processor
```python
from replicated import AsyncReplicatedClient, InstanceStatus
class TelemetryUploadProcessor(MaintenanceTaskProcessor):
"""Maintenance task processor for uploading metrics to Replicated."""
replicated_publishable_key: str
replicated_app_slug: str
async def __call__(self, task: MaintenanceTask) -> dict:
"""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()
if not pending_metrics:
return {"status": "no_pending_metrics"}
# Get admin email - skip if not available
admin_email = self._get_admin_email()
if not admin_email:
logger.info("Skipping telemetry upload - no admin email available")
return {
"status": "skipped",
"reason": "no_admin_email",
"total_processed": 0
}
uploaded_count = 0
failed_count = 0
async with AsyncReplicatedClient(
publishable_key=self.replicated_publishable_key,
app_slug=self.replicated_app_slug
) as client:
# Get or create customer and instance
customer = await client.customer.get_or_create(
email_address=admin_email
)
instance = await customer.get_or_create_instance()
# Store customer/instance IDs for future use
await self._update_telemetry_identity(customer.customer_id, instance.instance_id)
# Upload each metric batch
for metric_record in pending_metrics:
try:
# Send individual metrics
for key, value in metric_record.metrics_data.items():
await instance.send_metric(key, value)
# Update instance status
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()
if record:
record.uploaded_at = datetime.now(timezone.utc)
session.commit()
uploaded_count += 1
except Exception as e:
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()
if record:
record.upload_attempts += 1
record.last_upload_error = str(e)
session.commit()
failed_count += 1
# Note: No need to track last_successful_upload_at separately
# Can be derived from MAX(uploaded_at) in telemetry_metrics
return {
"status": "completed",
"uploaded": uploaded_count,
"failed": failed_count,
"total_processed": len(pending_metrics)
}
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')
if env_admin_email:
logger.info("Using admin email from environment variable")
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()
if first_user and first_user.email:
logger.info(f"Using first active user email: {first_user.email}")
return first_user.email
# No admin email available - skip telemetry
logger.info("No admin email available - skipping telemetry collection")
return None
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()
if not identity:
identity = TelemetryIdentity()
session.add(identity)
identity.customer_id = customer_id
identity.instance_id = instance_id
session.commit()
```
### 4.4 License Warning System
#### 4.4.1 License Status Endpoint
```python
from fastapi import APIRouter
from datetime import datetime, timezone, timedelta
license_router = APIRouter()
@license_router.get("/license-status")
async def get_license_status():
"""Get license warning status for UI display."""
# Only show warnings for OHE installations
if not _is_openhands_enterprise():
return {"warn": False, "message": ""}
with 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()
if not last_upload:
# No successful uploads yet - show warning after 4 days
return {
"warn": True,
"message": "OpenHands Enterprise license verification pending. Please ensure network connectivity."
}
# Check if last successful upload was more than 4 days ago
days_since_upload = (datetime.now(timezone.utc) - last_upload).days
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()
if oldest_unsent:
# Calculate expiration date (oldest unsent + 34 days)
expiration_date = oldest_unsent.collected_at + timedelta(days=34)
days_until_expiration = (expiration_date - datetime.now(timezone.utc)).days
if days_until_expiration <= 0:
message = "Your OpenHands Enterprise license has expired. Please contact support immediately."
else:
message = f"Your OpenHands Enterprise license will expire in {days_until_expiration} days. Please contact support if this issue persists."
return {"warn": True, "message": message}
return {"warn": False, "message": ""}
def _is_openhands_enterprise() -> bool:
"""Detect if this is an OHE installation."""
# Check for required OHE environment variables
required_vars = [
'GITHUB_APP_CLIENT_ID',
'KEYCLOAK_SERVER_URL',
'KEYCLOAK_REALM_NAME'
]
return all(os.getenv(var) for var in required_vars)
```
#### 4.4.2 UI Integration
The frontend will poll the license status endpoint and display warnings using the existing banner component pattern:
```typescript
// New component: LicenseWarningBanner.tsx
interface LicenseStatus {
warn: boolean;
message: string;
}
export function LicenseWarningBanner() {
const [licenseStatus, setLicenseStatus] = useState<LicenseStatus>({ warn: false, message: "" });
useEffect(() => {
const checkLicenseStatus = async () => {
try {
const response = await fetch('/api/license-status');
const status = await response.json();
setLicenseStatus(status);
} catch (error) {
console.error('Failed to check license status:', error);
}
};
// Check immediately and then every hour
checkLicenseStatus();
const interval = setInterval(checkLicenseStatus, 60 * 60 * 1000);
return () => clearInterval(interval);
}, []);
if (!licenseStatus.warn) {
return null;
}
return (
<div className="bg-red-600 text-white p-4 rounded flex items-center justify-between">
<div className="flex items-center">
<FaExclamationTriangle className="mr-3" />
<span>{licenseStatus.message}</span>
</div>
</div>
);
}
```
### 4.5 Cronjob Configuration
The cronjob configurations will be deployed via the OpenHands-Cloud helm charts.
#### 4.5.1 Collection Cronjob
The collection cronjob runs weekly to gather metrics:
```yaml
# charts/openhands/templates/telemetry-collection-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "openhands.fullname" . }}-telemetry-collection
labels:
{{- include "openhands.labels" . | nindent 4 }}
spec:
schedule: "0 2 * * 0" # Weekly on Sunday at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: telemetry-collector
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
env:
{{- include "openhands.env" . | nindent 12 }}
command:
- python
- -c
- |
from enterprise.storage.maintenance_task import MaintenanceTask, MaintenanceTaskStatus
from enterprise.storage.database import 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
with session_maker() as session:
session.add(task)
session.commit()
restartPolicy: OnFailure
```
#### 4.5.2 Upload Cronjob
The upload cronjob runs daily to send metrics to Replicated:
```yaml
# charts/openhands/templates/telemetry-upload-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "openhands.fullname" . }}-telemetry-upload
labels:
{{- include "openhands.labels" . | nindent 4 }}
spec:
schedule: "0 3 * * *" # Daily at 3 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: telemetry-uploader
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
env:
{{- include "openhands.env" . | nindent 12 }}
- name: REPLICATED_PUBLISHABLE_KEY
valueFrom:
secretKeyRef:
name: {{ include "openhands.fullname" . }}-replicated-config
key: publishable-key
- name: REPLICATED_APP_SLUG
value: {{ .Values.telemetry.replicatedAppSlug | default "openhands-enterprise" | quote }}
command:
- python
- -c
- |
from enterprise.storage.maintenance_task import MaintenanceTask, MaintenanceTaskStatus
from enterprise.storage.database import 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
with session_maker() as session:
session.add(task)
session.commit()
restartPolicy: OnFailure
```
## 5. Implementation Plan
All implementation must pass existing lints and tests. New functionality requires comprehensive unit tests with >90% coverage. Integration tests should verify end-to-end telemetry flow including collection, storage, upload, and warning display.
### 5.1 Database Schema and Models (M1)
**Repository**: OpenHands
Establish the foundational database schema and SQLAlchemy models for telemetry data storage.
#### 5.1.1 OpenHands - Database Migration
- [ ] `enterprise/migrations/versions/077_create_telemetry_tables.py`
- [ ] `enterprise/storage/telemetry_metrics.py`
- [ ] `enterprise/storage/telemetry_config.py`
#### 5.1.2 OpenHands - Model Tests
- [ ] `enterprise/tests/unit/storage/test_telemetry_metrics.py`
- [ ] `enterprise/tests/unit/storage/test_telemetry_config.py`
**Demo**: Database tables created and models can store/retrieve telemetry data.
### 5.2 Metrics Collection Framework (M2)
**Repository**: OpenHands
Implement the pluggable metrics collection system with registry and base classes.
#### 5.2.1 OpenHands - Core Collection Framework
- [ ] `enterprise/server/telemetry/__init__.py`
- [ ] `enterprise/server/telemetry/collector_base.py`
- [ ] `enterprise/server/telemetry/collector_registry.py`
- [ ] `enterprise/server/telemetry/decorators.py`
#### 5.2.2 OpenHands - Example Collectors
- [ ] `enterprise/server/telemetry/collectors/__init__.py`
- [ ] `enterprise/server/telemetry/collectors/system_metrics.py`
- [ ] `enterprise/server/telemetry/collectors/user_activity.py`
#### 5.2.3 OpenHands - Framework Tests
- [ ] `enterprise/tests/unit/telemetry/test_collector_base.py`
- [ ] `enterprise/tests/unit/telemetry/test_collector_registry.py`
- [ ] `enterprise/tests/unit/telemetry/test_system_metrics.py`
**Demo**: Developers can create new collectors with a single file change using the @register_collector decorator.
### 5.3 Collection and Upload Processors (M3)
**Repository**: OpenHands
Implement maintenance task processors for collecting metrics and uploading to Replicated.
#### 5.3.1 OpenHands - Collection Processor
- [ ] `enterprise/server/telemetry/collection_processor.py`
- [ ] `enterprise/tests/unit/telemetry/test_collection_processor.py`
#### 5.3.2 OpenHands - Upload Processor
- [ ] `enterprise/server/telemetry/upload_processor.py`
- [ ] `enterprise/tests/unit/telemetry/test_upload_processor.py`
#### 5.3.3 OpenHands - Integration Tests
- [ ] `enterprise/tests/integration/test_telemetry_flow.py`
**Demo**: Metrics are automatically collected weekly and uploaded daily to Replicated vendor portal.
### 5.4 License Warning API (M4)
**Repository**: OpenHands
Implement the license status endpoint for the warning system.
#### 5.4.1 OpenHands - License Status API
- [ ] `enterprise/server/routes/license.py`
- [ ] `enterprise/tests/unit/routes/test_license.py`
#### 5.4.2 OpenHands - API Integration
- [ ] Update `enterprise/saas_server.py` to include license router
**Demo**: License status API returns warning status based on telemetry upload success.
### 5.5 UI Warning Banner (M5)
**Repository**: OpenHands
Implement the frontend warning banner component and integration.
#### 5.5.1 OpenHands - UI Warning Banner
- [ ] `frontend/src/components/features/license/license-warning-banner.tsx`
- [ ] `frontend/src/components/features/license/license-warning-banner.test.tsx`
#### 5.5.2 OpenHands - UI Integration
- [ ] Update main UI layout to include license warning banner
- [ ] Add license status polling service
**Demo**: License warnings appear in UI when telemetry uploads fail for >4 days, with accurate expiration countdown.
### 5.6 Helm Chart Deployment Configuration (M6)
**Repository**: OpenHands-Cloud
Create Kubernetes cronjob configurations and deployment scripts.
#### 5.6.1 OpenHands-Cloud - Cronjob Manifests
- [ ] `charts/openhands/templates/telemetry-collection-cronjob.yaml`
- [ ] `charts/openhands/templates/telemetry-upload-cronjob.yaml`
#### 5.6.2 OpenHands-Cloud - Configuration Management
- [ ] `charts/openhands/templates/replicated-secret.yaml`
- [ ] Update `charts/openhands/values.yaml` with telemetry configuration options:
```yaml
# Add to values.yaml
telemetry:
enabled: true
replicatedAppSlug: "openhands-enterprise"
adminEmail: "" # Optional: admin email for customer identification
# Add to deployment environment variables
env:
OPENHANDS_ADMIN_EMAIL: "{{ .Values.telemetry.adminEmail }}"
```
**Demo**: Complete telemetry system deployed via helm chart with configurable collection intervals and Replicated integration.
### 5.7 Documentation and Enhanced Collectors (M7)
**Repository**: OpenHands
Add comprehensive metrics collectors, monitoring capabilities, and documentation.
#### 5.7.1 OpenHands - Advanced Collectors
- [ ] `enterprise/server/telemetry/collectors/conversation_metrics.py`
- [ ] `enterprise/server/telemetry/collectors/integration_usage.py`
- [ ] `enterprise/server/telemetry/collectors/performance_metrics.py`
#### 5.7.2 OpenHands - Monitoring and Testing
- [ ] `enterprise/server/telemetry/monitoring.py`
- [ ] `enterprise/tests/e2e/test_telemetry_system.py`
- [ ] Performance tests for large-scale metric collection
#### 5.7.3 OpenHands - Technical Documentation
- [ ] `enterprise/server/telemetry/README.md`
- [ ] Update deployment documentation with telemetry configuration instructions
- [ ] Add troubleshooting guide for telemetry issues
**Demo**: Rich telemetry data flowing to vendor portal with comprehensive monitoring, alerting for system health, and complete documentation.

View File

@@ -0,0 +1,207 @@
#!/usr/bin/env python
"""
This script can be removed once orgs is established - probably after Feb 15 2026
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

@@ -0,0 +1,274 @@
# Instructions for developing SAAS locally
You have a few options here, which are expanded on below:
- A simple local development setup, with live reloading for both OpenHands and this repo
- A more complex setup that includes Redis
- An even more complex setup that includes GitHub events
## Prerequisites
Before starting, make sure you have the following tools installed:
### Required for all options:
- [gcloud CLI](https://cloud.google.com/sdk/docs/install) - For authentication and secrets management
- [sops](https://github.com/mozilla/sops) - For secrets decryption
- macOS: `brew install sops`
- Linux: `sudo apt-get install sops` or download from GitHub releases
- Windows: Install via Chocolatey `choco install sops` or download from GitHub releases
### Additional requirements for enabling GitHub webhook events
- make
- Python development tools (build-essential, python3-dev)
- [ngrok](https://ngrok.com/download) - For creating tunnels to localhost
## Option 1: Simple local development
This option will allow you to modify both the OpenHands code and the code in this repo,
and see the changes in real-time.
This option works best for most scenarios. The only thing it's missing is
the GitHub events webhook, which is not necessary for most development.
### 1. OpenHands location
The open source OpenHands repo should be cloned as a sibling directory,
in `../OpenHands`. This is hard-coded in the pyproject.toml (edit if necessary)
If you're doing this the first time, you may need to run
```
poetry update openhands-ai
```
### 2. Set up env
First run this to retrieve Github App secrets
```
gcloud auth application-default login
gcloud config set project global-432717
enterprise_local/decrypt_env.sh /path/to/root/of/deploy/repo
```
Now run this to generate a `.env` file, which will used to run SAAS locally
```
python -m pip install PyYAML
export LITE_LLM_API_KEY=<your LLM API key>
python enterprise_local/convert_to_env.py
```
You'll also need to set up the runtime image, so that the dev server doesn't try to rebuild it.
```
export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:main-nikolaik
docker pull $SANDBOX_RUNTIME_CONTAINER_IMAGE
```
By default the application will log in json, you can override.
```
export LOG_PLAIN_TEXT=1
```
### 3. Start the OpenHands frontend
Start the frontend like you normally would in the open source OpenHands repo.
### 4. Start the SaaS backend
```
make build
make start-backend
```
You should have a server running on `localhost:3000`, similar to the open source backend.
Oauth should work properly.
## Option 2: With Redis
Follow all the steps above, then setup redis:
```bash
docker run -p 6379:6379 --name openhands-redis -d redis
export REDIS_HOST=host.docker.internal # you may want this to be localhost
export REDIS_PORT=6379
```
## Option 3: Work with GitHub events
### 1. Setup env file
(see above)
### 2. Build OpenHands
Develop on [Openhands](https://github.com/OpenHands/OpenHands) locally. When ready, run the following inside Openhands repo (not the Deploy repo)
```
docker build -f containers/app/Dockerfile -t openhands .
```
### 3. Build SAAS Openhands
Build the SAAS image locally inside Deploy repo. Note that `openhands` is the name of the image built in Step 2
```
docker build -t openhands-saas ./app/ --build-arg BASE="openhands"
```
### 4. Create a tunnel
Run in a separate terminal
```
ngrok http 3000
```
There will be a line
```
Forwarding https://bc71-2603-7000-5000-1575-e4a6-697b-589e-5801.ngrok-free.app
```
Remember this URL as it will be used in Step 5 and 6
### 5. Setup Staging Github App callback/webhook urls
Using the URL found in Step 4, add another callback URL (`https://bc71-2603-7000-5000-1575-e4a6-697b-589e-5801.ngrok-free.app/oauth/github/callback`)
### 6. Run
This is the last step! Run SAAS openhands locally using
```
docker run --env-file ./app/.env -p 3000:3000 openhands-saas
```
Note `--env-file` is what injects the `.env` file created in Step 1
Visit the tunnel domain found in Step 4 to run the app (`https://bc71-2603-7000-5000-1575-e4a6-697b-589e-5801.ngrok-free.app`)
### Local Debugging with VSCode
Local Development necessitates running a version of OpenHands that is as similar as possible to the version running in the SAAS Environment. Before running these steps, it is assumed you have a local development version of OpenHands running.
#### Redis
A Local redis instance is required for clustered communication between server nodes. The standard docker instance will suffice.
`docker run -it -p 6379:6379 --name my-redis -d redis`
#### Postgres
A Local postgres instance is required. I used the official docker image:
`docker run -p 5432:5432 --name my-postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=openhands -d postgres`
Run the alembic migrations:
`poetry run alembic upgrade head `
#### VSCode launch.json
The VSCode launch.json below sets up 2 servers to test clustering, running independently on localhost:3030 and localhost:3031. Running only the server on 3030 is usually sufficient unless tests of the clustered functionality are required. Secrets may be harvested directly from staging by connecting...
`kubectl exec --stdin --tty <POD_NAME> -n <NAMESPACE> -- /bin/bash`
And then invoking `printenv`. NOTE: _DO NOT DO THIS WITH PROD!!!_ (Hopefully by the time you read this, nobody will have access.)
```
{
"configurations": [
{
"name": "Python Debugger: Python File",
"type": "debugpy",
"request": "launch",
"program": "${file}"
},
{
"name": "OpenHands Deploy",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"saas_server:app",
"--reload",
"--host",
"0.0.0.0",
"--port",
"3030"
],
"env": {
"DEBUG": "1",
"FILE_STORE": "local",
"REDIS_HOST": "localhost:6379",
"OPENHANDS": "<YOUR LOCAL OPENHANDS DIR>",
"FRONTEND_DIRECTORY": "<YOUR LOCAL OPENHANDS DIR>/frontend/build",
"SANDBOX_RUNTIME_CONTAINER_IMAGE": "ghcr.io/openhands/runtime:main-nikolaik",
"FILE_STORE_PATH": "<YOUR HOME DIRECTORY>>/.openhands-state",
"OPENHANDS_CONFIG_CLS": "server.config.SaaSServerConfig",
"GITHUB_APP_ID": "1062351",
"GITHUB_APP_PRIVATE_KEY": "<GITHUB PRIVATE KEY>",
"GITHUB_APP_CLIENT_ID": "Iv23lis7eUWDQHIq8US0",
"GITHUB_APP_CLIENT_SECRET": "<GITHUB CLIENT SECRET>",
"POSTHOG_CLIENT_KEY": "<POSTHOG CLIENT KEY>",
"LITE_LLM_API_URL": "https://llm-proxy.staging.all-hands.dev",
"LITE_LLM_TEAM_ID": "62ea39c4-8886-44f3-b7ce-07ed4fe42d2c",
"LITE_LLM_API_KEY": "<LITE LLM API KEY>"
},
"justMyCode": false,
"cwd": "${workspaceFolder}/app"
},
{
"name": "OpenHands Deploy 2",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"saas_server:app",
"--reload",
"--host",
"0.0.0.0",
"--port",
"3031"
],
"env": {
"DEBUG": "1",
"FILE_STORE": "local",
"REDIS_HOST": "localhost:6379",
"OPENHANDS": "<YOUR LOCAL OPENHANDS DIR>",
"FRONTEND_DIRECTORY": "<YOUR LOCAL OPENHANDS DIR>/frontend/build",
"SANDBOX_RUNTIME_CONTAINER_IMAGE": "ghcr.io/openhands/runtime:main-nikolaik",
"FILE_STORE_PATH": "<YOUR HOME DIRECTORY>>/.openhands-state",
"OPENHANDS_CONFIG_CLS": "server.config.SaaSServerConfig",
"GITHUB_APP_ID": "1062351",
"GITHUB_APP_PRIVATE_KEY": "<GITHUB PRIVATE KEY>",
"GITHUB_APP_CLIENT_ID": "Iv23lis7eUWDQHIq8US0",
"GITHUB_APP_CLIENT_SECRET": "<GITHUB CLIENT SECRET>",
"POSTHOG_CLIENT_KEY": "<POSTHOG CLIENT KEY>",
"LITE_LLM_API_URL": "https://llm-proxy.staging.all-hands.dev",
"LITE_LLM_TEAM_ID": "62ea39c4-8886-44f3-b7ce-07ed4fe42d2c",
"LITE_LLM_API_KEY": "<LITE LLM API KEY>"
},
"justMyCode": false,
"cwd": "${workspaceFolder}/app"
},
{
"name": "Unit Tests",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": [
"./tests/unit",
//"./tests/unit/test_clustered_conversation_manager.py",
"--durations=0"
],
"env": {
"DEBUG": "1"
},
"justMyCode": false,
"cwd": "${workspaceFolder}/app"
},
// set working directory...
]
}
```

View File

@@ -0,0 +1,127 @@
import base64
import os
import sys
import yaml
def convert_yaml_to_env(yaml_file, target_parameters, output_env_file, prefix):
"""Converts a YAML file into .env file format for specified target parameters under 'stringData' and 'data'.
:param yaml_file: Path to the YAML file.
:param target_parameters: List of keys to extract from the YAML file.
:param output_env_file: Path to the output .env file.
:param prefix: Prefix for environment variables.
"""
try:
# Load the YAML file
with open(yaml_file, 'r') as file:
yaml_data = yaml.safe_load(file)
# Extract sections
string_data = yaml_data.get('stringData', None)
data = yaml_data.get('data', None)
if string_data:
env_source = string_data
process_base64 = False
elif data:
env_source = data
process_base64 = True
else:
print(
"Error: Neither 'stringData' nor 'data' section found in the YAML file."
)
return
env_lines = []
for param in target_parameters:
if param in env_source:
value = env_source[param]
if process_base64:
try:
decoded_value = base64.b64decode(value).decode('utf-8')
formatted_value = (
decoded_value.replace('\n', '\\n')
if '\n' in decoded_value
else decoded_value
)
except Exception as decode_error:
print(f"Error decoding base64 for '{param}': {decode_error}")
continue
else:
formatted_value = (
value.replace('\n', '\\n')
if isinstance(value, str) and '\n' in value
else value
)
new_key = prefix + param.upper().replace('-', '_')
env_lines.append(f'{new_key}={formatted_value}')
else:
print(
f"Warning: Parameter '{param}' not found in the selected section."
)
# Write to the .env file
with open(output_env_file, 'a') as env_file:
env_file.write('\n'.join(env_lines) + '\n')
except Exception as e:
print(f'Error: {e}')
lite_llm_api_key = os.getenv('LITE_LLM_API_KEY')
if not lite_llm_api_key:
print('Set the LITE_LLM_API_KEY environment variable to your API key')
sys.exit(1)
yaml_file = 'github_decrypted.yaml'
target_parameters = ['client-id', 'client-secret', 'webhook-secret', 'private-key']
output_env_file = './enterprise/.env'
if os.path.exists(output_env_file):
os.remove(output_env_file)
convert_yaml_to_env(yaml_file, target_parameters, output_env_file, 'GITHUB_APP_')
os.remove(yaml_file)
yaml_file = 'keycloak_realm_decrypted.yaml'
target_parameters = ['client-id', 'client-secret', 'provider-name', 'realm-name']
convert_yaml_to_env(yaml_file, target_parameters, output_env_file, 'KEYCLOAK_')
os.remove(yaml_file)
yaml_file = 'keycloak_admin_decrypted.yaml'
target_parameters = ['admin-password']
convert_yaml_to_env(yaml_file, target_parameters, output_env_file, 'KEYCLOAK_')
os.remove(yaml_file)
lines = []
lines.append('KEYCLOAK_SERVER_URL=https://auth.staging.all-hands.dev/')
lines.append('KEYCLOAK_SERVER_URL_EXT=https://auth.staging.all-hands.dev/')
lines.append('OPENHANDS_CONFIG_CLS=server.config.SaaSServerConfig')
lines.append(
'OPENHANDS_GITHUB_SERVICE_CLS=integrations.github.github_service.SaaSGitHubService'
)
lines.append(
'OPENHANDS_GITLAB_SERVICE_CLS=integrations.gitlab.gitlab_service.SaaSGitLabService'
)
lines.append(
'OPENHANDS_BITBUCKET_SERVICE_CLS=integrations.bitbucket.bitbucket_service.SaaSBitBucketService'
)
lines.append(
'OPENHANDS_CONVERSATION_VALIDATOR_CLS=storage.saas_conversation_validator.SaasConversationValidator'
)
lines.append('POSTHOG_CLIENT_KEY=test')
lines.append('ENABLE_PROACTIVE_CONVERSATION_STARTERS=true')
lines.append('MAX_CONCURRENT_CONVERSATIONS=10')
lines.append('LITE_LLM_API_URL=https://llm-proxy.eval.all-hands.dev')
lines.append('LITELLM_DEFAULT_MODEL=litellm_proxy/claude-opus-4-5-20251101')
lines.append(f'LITE_LLM_API_KEY={lite_llm_api_key}')
lines.append('LOCAL_DEPLOYMENT=true')
lines.append('DB_HOST=localhost')
with open(output_env_file, 'a') as env_file:
env_file.write('\n'.join(lines))
print(f'.env file created at: {output_env_file}')

View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
# Check if DEPLOY_DIR argument was provided
if [ $# -lt 1 ]; then
echo "Usage: $0 <DEPLOY_DIR>"
echo "Example: $0 /path/to/root/of/deploy/repo"
exit 1
fi
# Normalize path (remove trailing slash)
DEPLOY_DIR="${1%/}"
# Function to decrypt and rename
decrypt_and_move() {
local secret_path="$1"
local output_name="$2"
${DEPLOY_DIR}/scripts/decrypt.sh "${DEPLOY_DIR}/${secret_path}"
mv decrypted.yaml "${output_name}"
echo "Moved decrypted.yaml to ${output_name}"
}
# Decrypt each secret file
decrypt_and_move "openhands/envs/feature/secrets/github-app.yaml" "github_decrypted.yaml"
decrypt_and_move "openhands/envs/staging/secrets/keycloak-realm.yaml" "keycloak_realm_decrypted.yaml"
decrypt_and_move "openhands/envs/staging/secrets/keycloak-admin.yaml" "keycloak_admin_decrypted.yaml"

View File

@@ -1,18 +1,39 @@
from uuid import UUID
from experiments.constants import (
ENABLE_EXPERIMENT_MANAGER,
EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
)
from experiments.experiment_versions import (
handle_condenser_max_step_experiment,
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
@@ -31,20 +52,7 @@ class SaaSExperimentManager(ExperimentManager):
"""
logger.debug(
'experiment_manager:run_conversation_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_conversation_variant_test:skipped',
extra={'reason': 'experiment_manager_disabled'},
)
return conversation_settings
# Apply conversation-scoped experiments
conversation_settings = handle_condenser_max_step_experiment(
user_id, conversation_id, conversation_settings
extra={'user_id': user_id, 'conversation_id': conversation_id},
)
return conversation_settings

View File

@@ -5,12 +5,18 @@ This module contains the handler for the condenser max step experiment that test
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
@@ -190,3 +196,37 @@ def handle_condenser_max_step_experiment(
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

@@ -6,7 +6,7 @@ from datetime import datetime
from enum import Enum
from typing import Any
from github import Github, GithubIntegration
from github import Auth, Github, GithubIntegration
from integrations.github.github_view import (
GithubIssue,
)
@@ -84,7 +84,7 @@ class GitHubDataCollector:
# self.full_saved_pr_path = 'github_data/prs/{}-{}/data.json'
self.full_saved_pr_path = 'prs/github/{}-{}/data.json'
self.github_integration = GithubIntegration(
GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
auth=Auth.AppAuth(GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY)
)
self.conversation_id = None
@@ -143,7 +143,7 @@ class GitHubDataCollector:
try:
installation_token = self._get_installation_access_token(installation_id)
with Github(installation_token) as github_client:
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(repo_name)
issue = repo.get_issue(issue_number)
comments = []
@@ -237,7 +237,7 @@ class GitHubDataCollector:
def _get_pr_commits(self, installation_id: str, repo_name: str, pr_number: int):
commits = []
installation_token = self._get_installation_access_token(installation_id)
with Github(installation_token) as github_client:
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(repo_name)
pr = repo.get_pull(pr_number)

View File

@@ -1,6 +1,6 @@
from types import MappingProxyType
from github import Github, GithubIntegration
from github import Auth, Github, GithubIntegration
from integrations.github.data_collector import GitHubDataCollector
from integrations.github.github_solvability import summarize_issue_solvability
from integrations.github.github_view import (
@@ -21,17 +21,25 @@ from integrations.utils import (
CONVERSATION_URL,
HOST_URL,
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.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
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.integrations.service_types import AuthenticationError
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.storage.data_models.secrets import Secrets
from openhands.utils.async_utils import call_sync_from_async
@@ -42,7 +50,7 @@ class GithubManager(Manager):
self.token_manager = token_manager
self.data_collector = data_collector
self.github_integration = GithubIntegration(
GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
auth=Auth.AppAuth(GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY)
)
self.jinja_env = Environment(
@@ -76,7 +84,7 @@ class GithubManager(Manager):
reaction: The reaction to add (e.g. "eyes", "+1", "-1", "laugh", "confused", "heart", "hooray", "rocket")
installation_token: GitHub installation access token for API access
"""
with Github(installation_token) as github_client:
with Github(auth=Auth.Token(installation_token)) as github_client:
repo = github_client.get_repo(github_view.full_repo_name)
# Add reaction based on view type
if isinstance(github_view, GithubInlinePRComment):
@@ -164,8 +172,13 @@ class GithubManager(Manager):
)
if await self.is_job_requested(message):
payload = message.message.get('payload', {})
user_id = payload['sender']['id']
keycloak_user_id = await self.token_manager.get_user_id_from_idp_user_id(
user_id, ProviderType.GITHUB
)
github_view = await GithubFactory.create_github_view_from_payload(
message, self.token_manager
message, keycloak_user_id
)
logger.info(
f'[GitHub] Creating job for {github_view.user_info.username} in {github_view.full_repo_name}#{github_view.issue_number}'
@@ -193,7 +206,7 @@ class GithubManager(Manager):
outgoing_message = message.message
if isinstance(github_view, GithubInlinePRComment):
with Github(installation_token) as github_client:
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(
@@ -205,7 +218,7 @@ class GithubManager(Manager):
or isinstance(github_view, GithubIssueComment)
or isinstance(github_view, GithubIssue)
):
with Github(installation_token) as github_client:
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)
@@ -250,7 +263,7 @@ class GithubManager(Manager):
f'[GitHub] Creating new conversation for user {user_info.username}'
)
secret_store = UserSecrets(
secret_store = Secrets(
provider_tokens=MappingProxyType(
{
ProviderType.GITHUB: ProviderToken(
@@ -282,8 +295,15 @@ class GithubManager(Manager):
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
)
await github_view.create_new_conversation(
self.jinja_env, secret_store.provider_tokens, convo_metadata
self.jinja_env,
secret_store.provider_tokens,
convo_metadata,
saas_user_auth,
)
conversation_id = github_view.conversation_id
@@ -292,18 +312,19 @@ class GithubManager(Manager):
f'[GitHub] Created conversation {conversation_id} for user {user_info.username}'
)
# Create a GithubCallbackProcessor
processor = GithubCallbackProcessor(
github_view=github_view,
send_summary_instruction=True,
)
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)
# Register the callback processor
register_callback_processor(conversation_id, processor)
logger.info(
f'[Github] Registered callback processor for conversation {conversation_id}'
)
logger.info(
f'[Github] Registered callback processor for conversation {conversation_id}'
)
# Send message with conversation link
conversation_link = CONVERSATION_URL.format(conversation_id)
@@ -328,6 +349,13 @@ class GithubManager(Manager):
msg_info = f'@{user_info.username} please set a valid LLM API key in [OpenHands Cloud]({HOST_URL}) before starting a job.'
except (AuthenticationError, ExpiredError, SessionExpiredError) as e:
logger.warning(
f'[GitHub] Session expired for user {user_info.username}: {str(e)}'
)
msg_info = get_session_expired_message(user_info.username)
msg = self.create_outgoing_message(msg_info)
await self.send_message(msg, github_view)

View File

@@ -1,6 +1,6 @@
import asyncio
from integrations.utils import store_repositories_in_db
from integrations.store_repo_utils import store_repositories_in_db
from pydantic import SecretStr
from server.auth.token_manager import TokenManager

View File

@@ -1,7 +1,7 @@
import asyncio
import time
from github import Github
from github import Auth, Github
from integrations.github.github_view import (
GithubInlinePRComment,
GithubIssueComment,
@@ -47,7 +47,7 @@ def fetch_github_issue_context(
context_parts.append(f'Title: {github_view.title}')
context_parts.append(f'Description:\n{github_view.description}')
with Github(user_token) as github_client:
with Github(auth=Auth.Token(user_token)) as github_client:
repo = github_client.get_repo(github_view.full_repo_name)
issue = repo.get_issue(github_view.issue_number)
if issue.labels:

View File

@@ -0,0 +1,293 @@
import logging
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 pydantic import Field
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
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 GithubV1CallbackProcessor(EventCallbackProcessor):
"""Callback processor for GitHub V1 integrations."""
github_view_data: dict[str, Any] = Field(default_factory=dict)
should_request_summary: bool = Field(default=True)
inline_pr_comment: bool = Field(default=False)
async def __call__(
self,
conversation_id: UUID,
callback: EventCallback,
event: Event,
) -> EventCallbackResult | None:
"""Process events for GitHub V1 integration."""
# Only handle ConversationStateUpdateEvent
if not isinstance(event, ConversationStateUpdateEvent):
return None
# Only act when execution has finished
if not (event.key == 'execution_status' and event.value == 'finished'):
return None
_logger.info('[GitHub V1] Callback agent state was %s', event)
_logger.info(
'[GitHub 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'[GitHub V1] Requesting summary {conversation_id}')
summary = await self._request_summary(conversation_id)
_logger.info(
f'[GitHub V1] Posting summary {conversation_id}',
extra={'summary': summary},
)
await self._post_summary_to_github(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('[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
)
return EventCallbackResult(
status=EventCallbackResultStatus.ERROR,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=str(e),
)
# -------------------------------------------------------------------------
# GitHub helpers
# -------------------------------------------------------------------------
def _get_installation_access_token(self) -> str:
installation_id = self.github_view_data.get('installation_id')
if not installation_id:
raise ValueError(
f'Missing installation ID for GitHub payload: {self.github_view_data}'
)
if not GITHUB_APP_CLIENT_ID or not GITHUB_APP_PRIVATE_KEY:
raise ValueError('GitHub App credentials are not configured')
github_integration = GithubIntegration(
auth=Auth.AppAuth(GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY),
)
token_data = github_integration.get_access_token(installation_id)
return token_data.token
async def _post_summary_to_github(self, summary: str) -> None:
"""Post a summary comment to the configured GitHub issue."""
installation_token = self._get_installation_access_token()
if not installation_token:
raise RuntimeError('Missing GitHub credentials')
full_repo_name = self.github_view_data['full_repo_name']
issue_number = self.github_view_data['issue_number']
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)
issue = repo.get_issue(number=issue_number)
issue.create_comment(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(
'[GitHub 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(
'[GitHub 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(
'[GitHub 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)
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,6 +1,7 @@
from uuid import uuid4
from dataclasses import dataclass
from uuid import UUID, uuid4
from github import Github, GithubIntegration
from github import Auth, Github, GithubIntegration
from github.Issue import Issue
from integrations.github.github_types import (
WorkflowRun,
@@ -8,32 +9,44 @@ from integrations.github.github_types import (
WorkflowRunStatus,
)
from integrations.models import Message
from integrations.resolver_context import ResolverUserContext
from integrations.types import ResolverViewInterface, UserData
from integrations.utils import (
ENABLE_PROACTIVE_CONVERSATION_STARTERS,
ENABLE_V1_GITHUB_RESOLVER,
HOST,
HOST_URL,
get_oh_labels,
get_user_v1_enabled_setting,
has_exact_mention,
)
from jinja2 import Environment
from pydantic.dataclasses import dataclass
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_secrets_store import SaasSecretsStore
from storage.user_settings import UserSettings
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.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,
@@ -43,6 +56,10 @@ from openhands.utils.async_utils import call_sync_from_async
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
async def is_v1_enabled_for_github_resolver(user_id: str) -> bool:
return await get_user_v1_enabled_setting(user_id) and ENABLE_V1_GITHUB_RESOLVER
async def get_user_proactive_conversation_setting(user_id: str | None) -> bool:
"""Get the user's proactive conversation setting.
@@ -61,18 +78,15 @@ async def get_user_proactive_conversation_setting(user_id: str | None) -> bool:
if not user_id:
return False
# Check global setting first - if disabled globally, return False
if not ENABLE_PROACTIVE_CONVERSATION_STARTERS:
return False
def _get_setting():
with session_maker() as session:
settings = (
session.query(UserSettings)
.filter(UserSettings.keycloak_user_id == user_id)
.first()
)
if not settings or settings.enable_proactive_conversation_starters is None:
return False
return settings.enable_proactive_conversation_starters
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)
@@ -97,6 +111,10 @@ class GithubIssue(ResolverViewInterface):
title: str
description: str
previous_comments: list[Comment]
v1_enabled: bool
def _get_branch_name(self) -> str | None:
return getattr(self, 'branch_name', None)
async def _load_resolver_context(self):
github_service = GithubServiceImpl(
@@ -131,6 +149,7 @@ class GithubIssue(ResolverViewInterface):
issue_body=self.description,
previous_comments=self.previous_comments,
)
return user_instructions, conversation_instructions
async def _get_user_secrets(self):
@@ -143,14 +162,33 @@ class GithubIssue(ResolverViewInterface):
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
)
logger.info(
f'[GitHub V1]: User flag found for {self.user_info.keycloak_user_id} is {self.v1_enabled}'
)
if self.v1_enabled:
# Create dummy conversationm 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,
)
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=None,
selected_branch=self._get_branch_name(),
conversation_trigger=ConversationTrigger.RESOLVER,
git_provider=ProviderType.GITHUB,
)
self.conversation_id = conversation_metadata.conversation_id
return conversation_metadata
@@ -159,7 +197,29 @@ class GithubIssue(ResolverViewInterface):
jinja_env: Environment,
git_provider_tokens: PROVIDER_TOKEN_TYPE,
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}'
)
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()
user_instructions, conversation_instructions = await self._get_instructions(
@@ -178,6 +238,77 @@ class GithubIssue(ResolverViewInterface):
conversation_instructions=conversation_instructions,
)
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('[GitHub V1]: Creating V1 conversation')
user_instructions, conversation_instructions = await self._get_instructions(
jinja_env
)
# Create the initial message request
initial_message = SendMessageRequest(
role='user', content=[TextContent(text=user_instructions)]
)
# Create the GitHub V1 callback processor
github_callback_processor = self._create_github_v1_callback_processor()
# Get the app conversation service and start the conversation
injector_state = InjectorState()
# 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.GITHUB,
title=f'GitHub Issue #{self.issue_number}: {self.title}',
trigger=ConversationTrigger.RESOLVER,
processors=[
github_callback_processor
], # Pass the callback processor directly
)
# Set up the GitHub user context for the V1 system
github_user_context = ResolverUserContext(saas_user_auth=saas_user_auth)
setattr(injector_state, USER_CONTEXT_ATTR, github_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_github_v1_callback_processor(self):
"""Create a V1 callback processor for GitHub integration."""
from integrations.github.github_v1_callback_processor import (
GithubV1CallbackProcessor,
)
# Create and return the GitHub V1 callback processor
return GithubV1CallbackProcessor(
github_view_data={
'issue_number': self.issue_number,
'full_repo_name': self.full_repo_name,
'installation_id': self.installation_id,
},
send_summary_instruction=self.send_summary_instruction,
)
@dataclass
class GithubIssueComment(GithubIssue):
@@ -196,7 +327,6 @@ class GithubIssueComment(GithubIssue):
conversation_instructions_template = jinja_env.get_template(
'issue_conversation_instructions.j2'
)
conversation_instructions = conversation_instructions_template.render(
issue_number=self.issue_number,
issue_title=self.title,
@@ -232,20 +362,6 @@ class GithubPRComment(GithubIssueComment):
return user_instructions, conversation_instructions
async def initialize_new_conversation(self) -> ConversationMetadata:
# FIXME: Handle if initialize_conversation returns None
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.branch_name,
conversation_trigger=ConversationTrigger.RESOLVER,
git_provider=ProviderType.GITHUB,
)
self.conversation_id = conversation_metadata.conversation_id
return conversation_metadata
@dataclass
class GithubInlinePRComment(GithubPRComment):
@@ -280,7 +396,6 @@ class GithubInlinePRComment(GithubPRComment):
conversation_instructions_template = jinja_env.get_template(
'pr_update_conversation_instructions.j2'
)
conversation_instructions = conversation_instructions_template.render(
pr_number=self.issue_number,
pr_title=self.title,
@@ -293,6 +408,24 @@ class GithubInlinePRComment(GithubPRComment):
return user_instructions, conversation_instructions
def _create_github_v1_callback_processor(self):
"""Create a V1 callback processor for GitHub integration."""
from integrations.github.github_v1_callback_processor import (
GithubV1CallbackProcessor,
)
# Create and return the GitHub V1 callback processor
return GithubV1CallbackProcessor(
github_view_data={
'issue_number': self.issue_number,
'full_repo_name': self.full_repo_name,
'installation_id': self.installation_id,
'comment_id': self.comment_id,
},
inline_pr_comment=True,
send_summary_instruction=self.send_summary_instruction,
)
@dataclass
class GithubFailingAction:
@@ -542,13 +675,13 @@ class GithubFactory:
def _interact_with_github() -> Issue | None:
with GithubIntegration(
GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
auth=Auth.AppAuth(GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY)
) as integration:
access_token = integration.get_access_token(
payload['installation']['id']
).token
with Github(access_token) as gh:
with Github(auth=Auth.Token(access_token)) as gh:
repo = gh.get_repo(selected_repo)
login = (
payload['organization']['login']
@@ -606,7 +739,7 @@ class GithubFactory:
@staticmethod
async def create_github_view_from_payload(
message: Message, token_manager: TokenManager
message: Message, keycloak_user_id: str
) -> ResolverViewInterface:
"""Create the appropriate class (GithubIssue or GithubPRComment) based on the payload.
Also return metadata about the event (e.g., action type).
@@ -616,17 +749,10 @@ class GithubFactory:
user_id = payload['sender']['id']
username = payload['sender']['login']
keyloak_user_id = await token_manager.get_user_id_from_idp_user_id(
user_id, ProviderType.GITHUB
)
if keyloak_user_id is None:
logger.warning(f'Got invalid keyloak user id for GitHub User {user_id} ')
selected_repo = GithubFactory.get_full_repo_name(repo_obj)
is_public_repo = not repo_obj.get('private', True)
user_info = UserData(
user_id=user_id, username=username, keycloak_user_id=keyloak_user_id
user_id=user_id, username=username, keycloak_user_id=keycloak_user_id
)
installation_id = message.message['installation']
@@ -650,6 +776,7 @@ class GithubFactory:
title='',
description='',
previous_comments=[],
v1_enabled=False,
)
elif GithubFactory.is_issue_comment(message):
@@ -675,6 +802,7 @@ class GithubFactory:
title='',
description='',
previous_comments=[],
v1_enabled=False,
)
elif GithubFactory.is_pr_comment(message):
@@ -685,12 +813,12 @@ class GithubFactory:
access_token = ''
with GithubIntegration(
GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
auth=Auth.AppAuth(GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY)
) as integration:
access_token = integration.get_access_token(installation_id).token
head_ref = None
with Github(access_token) as gh:
with Github(auth=Auth.Token(access_token)) as gh:
repo = gh.get_repo(selected_repo)
pull_request = repo.get_pull(issue_number)
head_ref = pull_request.head.ref
@@ -716,6 +844,7 @@ class GithubFactory:
title='',
description='',
previous_comments=[],
v1_enabled=False,
)
elif GithubFactory.is_inline_pr_comment(message):
@@ -749,6 +878,7 @@ class GithubFactory:
title='',
description='',
previous_comments=[],
v1_enabled=False,
)
else:

View File

@@ -15,6 +15,7 @@ from integrations.utils import (
CONVERSATION_URL,
HOST_URL,
OPENHANDS_RESOLVER_TEMPLATES_DIR,
get_session_expired_message,
)
from jinja2 import Environment, FileSystemLoader
from pydantic import SecretStr
@@ -24,8 +25,12 @@ 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
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.storage.data_models.secrets import Secrets
class GitlabManager(Manager):
@@ -198,7 +203,7 @@ class GitlabManager(Manager):
f'[GitLab] Creating new conversation for user {user_info.username}'
)
secret_store = UserSecrets(
secret_store = Secrets(
provider_tokens=MappingProxyType(
{
ProviderType.GITLAB: ProviderToken(
@@ -249,6 +254,13 @@ class GitlabManager(Manager):
msg_info = f'@{user_info.username} please set a valid LLM API key in [OpenHands Cloud]({HOST_URL}) before starting a job.'
except SessionExpiredError as e:
logger.warning(
f'[GitLab] Session expired for user {user_info.username}: {str(e)}'
)
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)

View File

@@ -1,7 +1,7 @@
import asyncio
from integrations.store_repo_utils import store_repositories_in_db
from integrations.types import GitLabResourceType
from integrations.utils import store_repositories_in_db
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from storage.gitlab_webhook import GitlabWebhook, WebhookStatus
@@ -80,22 +80,52 @@ class SaaSGitLabService(GitLabService):
logger.warning('external_auth_token and user_id not set!')
return gitlab_token
async def get_owned_groups(self) -> list[dict]:
async def get_owned_groups(self, min_access_level: int = 40) -> list[dict]:
"""
Get all groups for which the current user is the owner.
Get all top-level groups where the current user has admin access.
This method supports pagination and fetches all groups where the user has
at least the specified access level.
Args:
min_access_level: Minimum access level required (default: 40 for Maintainer or Owner)
- 40: Maintainer or Owner
- 50: Owner only
Returns:
list[dict]: A list of groups owned by the current user.
list[dict]: A list of groups where user has the specified access level or higher.
"""
url = f'{self.BASE_URL}/groups'
params = {'owned': 'true', 'per_page': 100, 'top_level_only': 'true'}
groups_with_admin_access = []
page = 1
per_page = 100
try:
response, headers = await self._make_request(url, params)
return response
except Exception:
logger.warning('Error fetching owned groups', exc_info=True)
return []
while True:
try:
url = f'{self.BASE_URL}/groups'
params = {
'page': str(page),
'per_page': str(per_page),
'min_access_level': min_access_level,
'top_level_only': 'true',
}
response, headers = await self._make_request(url, params)
if not response:
break
groups_with_admin_access.extend(response)
page += 1
# Check if we've reached the last page
link_header = headers.get('Link', '')
if 'rel="next"' not in link_header:
break
except Exception:
logger.warning(f'Error fetching groups on page {page}', exc_info=True)
break
return groups_with_admin_access
async def add_owned_projects_and_groups_to_db(self, owned_personal_projects):
"""
@@ -527,3 +557,55 @@ class SaaSGitLabService(GitLabService):
await self._make_request(url=url, params=params, method=RequestMethod.POST)
except Exception as e:
logger.exception(f'[GitLab]: Reply to MR failed {e}')
async def get_user_resources_with_admin_access(
self,
) -> tuple[list[dict], list[dict]]:
"""
Get all projects and groups where the current user has admin access (maintainer or owner).
Returns:
tuple[list[dict], list[dict]]: A tuple containing:
- list of projects where user has admin access
- list of groups where user has admin access
"""
projects_with_admin_access = []
groups_with_admin_access = []
# Fetch all projects the user is a member of
page = 1
per_page = 100
while True:
try:
url = f'{self.BASE_URL}/projects'
params = {
'page': str(page),
'per_page': str(per_page),
'membership': 1,
'min_access_level': 40, # Maintainer or Owner
}
response, headers = await self._make_request(url, params)
if not response:
break
projects_with_admin_access.extend(response)
page += 1
# Check if we've reached the last page
link_header = headers.get('Link', '')
if 'rel="next"' not in link_header:
break
except Exception:
logger.warning(f'Error fetching projects on page {page}', exc_info=True)
break
# Fetch all groups where user is owner or maintainer
groups_with_admin_access = await self.get_owned_groups(min_access_level=40)
logger.info(
f'Found {len(projects_with_admin_access)} projects and {len(groups_with_admin_access)} groups with admin access'
)
return projects_with_admin_access, groups_with_admin_access

View File

@@ -0,0 +1,199 @@
"""Shared utilities for GitLab webhook installation.
This module contains reusable functions and classes for installing GitLab webhooks
that can be used by both the cron job and API routes.
"""
from typing import cast
from uuid import uuid4
from integrations.types import GitLabResourceType
from integrations.utils import GITLAB_WEBHOOK_URL
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
# Webhook configuration constants
WEBHOOK_NAME = 'OpenHands Resolver'
SCOPES: list[str] = [
'note_events',
'merge_requests_events',
'confidential_issues_events',
'issues_events',
'confidential_note_events',
'job_events',
'pipeline_events',
]
class BreakLoopException(Exception):
"""Exception raised when webhook installation conditions are not met or rate limited."""
pass
async def verify_webhook_conditions(
gitlab_service: type[GitService],
resource_type: GitLabResourceType,
resource_id: str,
webhook_store: GitlabWebhookStore,
webhook: GitlabWebhook,
) -> None:
"""
Verify all conditions are met for webhook installation.
Raises BreakLoopException if any condition fails or rate limited.
Args:
gitlab_service: GitLab service instance
resource_type: Type of resource (PROJECT or GROUP)
resource_id: ID of the resource
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
)
logger.info(
'Does resource exists',
extra={
'does_resource_exist': does_resource_exist,
'status': status,
'resource_id': resource_id,
'resource_type': resource_type,
},
)
if status == WebhookStatus.RATE_LIMITED:
raise BreakLoopException()
if not does_resource_exist and status != WebhookStatus.RATE_LIMITED:
await webhook_store.delete_webhook(webhook)
raise BreakLoopException()
# Check if user has admin access
(
is_user_admin_of_resource,
status,
) = await gitlab_service.check_user_has_admin_access_to_resource(
resource_type, resource_id
)
logger.info(
'Is user admin',
extra={
'is_user_admin': is_user_admin_of_resource,
'status': status,
'resource_id': resource_id,
'resource_type': resource_type,
},
)
if status == WebhookStatus.RATE_LIMITED:
raise BreakLoopException()
if not is_user_admin_of_resource:
await webhook_store.delete_webhook(webhook)
raise BreakLoopException()
# Check if webhook already exists
(
does_webhook_exist_on_resource,
status,
) = await gitlab_service.check_webhook_exists_on_resource(
resource_type, resource_id, GITLAB_WEBHOOK_URL
)
logger.info(
'Does webhook already exist',
extra={
'does_webhook_exist_on_resource': does_webhook_exist_on_resource,
'status': status,
'resource_id': resource_id,
'resource_type': resource_type,
},
)
if status == WebhookStatus.RATE_LIMITED:
raise BreakLoopException()
if does_webhook_exist_on_resource != webhook.webhook_exists:
await webhook_store.update_webhook(
webhook, {'webhook_exists': does_webhook_exist_on_resource}
)
if does_webhook_exist_on_resource:
raise BreakLoopException()
async def install_webhook_on_resource(
gitlab_service: type[GitService],
resource_type: GitLabResourceType,
resource_id: str,
webhook_store: GitlabWebhookStore,
webhook: GitlabWebhook,
) -> tuple[str | None, WebhookStatus | None]:
"""
Install webhook on a GitLab resource.
Args:
gitlab_service: GitLab service instance
resource_type: Type of resource (PROJECT or GROUP)
resource_id: ID of the resource
webhook_store: Webhook store instance
webhook: Webhook object to install
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())}'
webhook_id, status = await gitlab_service.install_webhook(
resource_type=resource_type,
resource_id=resource_id,
webhook_name=WEBHOOK_NAME,
webhook_url=GITLAB_WEBHOOK_URL,
webhook_secret=webhook_secret,
webhook_uuid=webhook_uuid,
scopes=SCOPES,
)
logger.info(
'Creating new webhook',
extra={
'webhook_id': webhook_id,
'status': status,
'resource_id': resource_id,
'resource_type': resource_type,
},
)
if status == WebhookStatus.RATE_LIMITED:
raise BreakLoopException()
if webhook_id:
await webhook_store.update_webhook(
webhook=webhook,
update_fields={
'webhook_secret': webhook_secret,
'webhook_exists': True, # webhook was created
'webhook_url': GITLAB_WEBHOOK_URL,
'scopes': SCOPES,
'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}'
)
return webhook_id, status

View File

@@ -1,22 +1,38 @@
import hashlib
import hmac
from typing import Dict, Optional, Tuple
from urllib.parse import urlparse
"""Jira integration manager.
This module orchestrates the processing of Jira webhook events:
1. Parse webhook payload (via JiraPayloadParser)
2. Validate workspace
3. Authenticate user
4. Create view with repository selection (via JiraFactory)
5. Start conversation job
The manager delegates payload parsing to JiraPayloadParser and view creation
to JiraFactory, keeping the orchestration logic clean and traceable.
"""
import httpx
from fastapi import Request
from integrations.jira.jira_types import JiraViewInterface
from integrations.jira.jira_view import (
JiraExistingConversationView,
JiraFactory,
JiraNewConversationView,
from integrations.jira.jira_payload import (
JiraPayloadError,
JiraPayloadParser,
JiraPayloadSkipped,
JiraPayloadSuccess,
JiraWebhookPayload,
)
from integrations.jira.jira_types import (
JiraViewInterface,
RepositoryNotFoundError,
StartingConvoException,
)
from integrations.jira.jira_view import JiraFactory, JiraNewConversationView
from integrations.manager import Manager
from integrations.models import JobContext, Message
from integrations.models import Message
from integrations.utils import (
HOST,
HOST_URL,
OPENHANDS_RESOLVER_TEMPLATES_DIR,
filter_potential_repos_by_user_msg,
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
@@ -27,311 +43,221 @@ from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
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
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
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
# Get OH labels for this environment
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
class JiraManager(Manager):
"""Manager for processing Jira webhook events.
This class orchestrates the flow from webhook receipt to conversation creation,
delegating parsing to JiraPayloadParser and view creation to JiraFactory.
"""
def __init__(self, token_manager: TokenManager):
self.token_manager = token_manager
self.integration_store = JiraIntegrationStore.get_instance()
self.jinja_env = Environment(
loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR + 'jira')
)
self.payload_parser = JiraPayloadParser(
oh_label=OH_LABEL,
inline_oh_label=INLINE_OH_LABEL,
)
async def authenticate_user(
self, jira_user_id: str, workspace_id: int
async def receive_message(self, message: Message):
"""Process incoming Jira webhook message.
Flow:
1. Parse webhook payload
2. Validate workspace exists and is active
3. Authenticate user
4. Create view (includes fetching issue details and selecting repository)
5. Start job
Each step has clear logging for traceability.
"""
raw_payload = message.message.get('payload', {})
# Step 1: Parse webhook payload
logger.info(
'[Jira] Received webhook',
extra={'raw_payload': raw_payload},
)
parse_result = self.payload_parser.parse(raw_payload)
if isinstance(parse_result, JiraPayloadSkipped):
logger.info(
'[Jira] Webhook skipped', extra={'reason': parse_result.skip_reason}
)
return
if isinstance(parse_result, JiraPayloadError):
logger.warning(
'[Jira] Webhook parse failed', extra={'error': parse_result.error}
)
return
payload = parse_result.payload
logger.info(
'[Jira] Processing webhook',
extra={
'event_type': payload.event_type.value,
'issue_key': payload.issue_key,
'user_email': payload.user_email,
},
)
# Step 2: Validate workspace
workspace = await self._get_active_workspace(payload)
if not workspace:
return
# Step 3: Authenticate user
jira_user, saas_user_auth = await self._authenticate_user(payload, workspace)
if not jira_user or not saas_user_auth:
return
# Step 4: Create view (includes issue details fetch and repo selection)
decrypted_api_key = self.token_manager.decrypt_text(workspace.svc_acc_api_key)
try:
view = await JiraFactory.create_view(
payload=payload,
workspace=workspace,
user=jira_user,
user_auth=saas_user_auth,
decrypted_api_key=decrypted_api_key,
)
except RepositoryNotFoundError as e:
logger.warning(
'[Jira] Repository not found',
extra={'issue_key': payload.issue_key, 'error': str(e)},
)
await self._send_error_from_payload(payload, workspace, str(e))
return
except StartingConvoException as e:
logger.warning(
'[Jira] View creation failed',
extra={'issue_key': payload.issue_key, 'error': str(e)},
)
await self._send_error_from_payload(payload, workspace, str(e))
return
except Exception as e:
logger.error(
'[Jira] Unexpected error creating view',
extra={'issue_key': payload.issue_key, 'error': str(e)},
exc_info=True,
)
await self._send_error_from_payload(
payload,
workspace,
'Failed to initialize conversation. Please try again.',
)
return
# Step 5: Start job
await self.start_job(view)
async def _get_active_workspace(
self, payload: JiraWebhookPayload
) -> JiraWorkspace | None:
"""Validate and return the workspace for the webhook.
Returns None if:
- Workspace not found
- Workspace is inactive
- Request is from service account (to prevent recursion)
"""
workspace = await self.integration_store.get_workspace_by_name(
payload.workspace_name
)
if not workspace:
logger.warning(
'[Jira] Workspace not found',
extra={'workspace_name': payload.workspace_name},
)
# Can't send error without workspace credentials
return None
# Prevent recursive triggers from service account
if payload.user_email == workspace.svc_acc_email:
logger.debug(
'[Jira] Ignoring service account trigger',
extra={'workspace_name': payload.workspace_name},
)
return None
if workspace.status != 'active':
logger.warning(
'[Jira] Workspace inactive',
extra={'workspace_id': workspace.id, 'status': workspace.status},
)
await self._send_error_from_payload(
payload, workspace, 'Jira integration is not active for your workspace.'
)
return None
return workspace
async def _authenticate_user(
self, payload: JiraWebhookPayload, workspace: JiraWorkspace
) -> tuple[JiraUser | None, UserAuth | None]:
"""Authenticate Jira user and get their OpenHands user auth."""
# Find active Jira user by Keycloak user ID and workspace ID
"""Authenticate the Jira user and get OpenHands auth."""
jira_user = await self.integration_store.get_active_user(
jira_user_id, workspace_id
payload.account_id, workspace.id
)
if not jira_user:
logger.warning(
f'[Jira] No active Jira user found for {jira_user_id} in workspace {workspace_id}'
'[Jira] User not found or inactive',
extra={
'account_id': payload.account_id,
'user_email': payload.user_email,
'workspace_id': workspace.id,
},
)
await self._send_error_from_payload(
payload,
workspace,
f'User {payload.user_email} is not authenticated or active in the Jira integration.',
)
return None, None
saas_user_auth = await get_user_auth_from_keycloak_id(
jira_user.keycloak_user_id
)
if not saas_user_auth:
logger.warning(
'[Jira] Failed to get OpenHands auth',
extra={
'keycloak_user_id': jira_user.keycloak_user_id,
'user_email': payload.user_email,
},
)
await self._send_error_from_payload(
payload,
workspace,
f'User {payload.user_email} is not authenticated with OpenHands.',
)
return None, None
return jira_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 Jira webhook signature."""
signature_header = request.headers.get('x-hub-signature')
signature = signature_header.split('=')[1] if signature_header else None
body = await request.body()
payload = await request.json()
workspace_name = ''
if payload.get('webhookEvent') == 'comment_created':
selfUrl = payload.get('comment', {}).get('author', {}).get('self')
elif payload.get('webhookEvent') == 'jira:issue_updated':
selfUrl = payload.get('user', {}).get('self')
else:
workspace_name = ''
parsedUrl = urlparse(selfUrl)
if parsedUrl.hostname:
workspace_name = parsedUrl.hostname
if not workspace_name:
logger.warning('[Jira] No workspace name found in webhook payload')
return False, None, None
if not signature:
logger.warning('[Jira] 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('[Jira] Could not identify workspace for webhook')
return False, None, None
if workspace.status != 'active':
logger.warning(f'[Jira] 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('[Jira] Webhook signature verified successfully')
return True, signature, payload
return False, None, None
def parse_webhook(self, payload: Dict) -> JobContext | None:
event_type = payload.get('webhookEvent')
if event_type == 'comment_created':
comment_data = payload.get('comment', {})
comment = comment_data.get('body', '')
if '@openhands' not in comment:
return None
issue_data = payload.get('issue', {})
issue_id = issue_data.get('id')
issue_key = issue_data.get('key')
base_api_url = issue_data.get('self', '').split('/rest/')[0]
user_data = comment_data.get('author', {})
user_email = user_data.get('emailAddress')
display_name = user_data.get('displayName')
account_id = user_data.get('accountId')
elif event_type == 'jira:issue_updated':
changelog = payload.get('changelog', {})
items = changelog.get('items', [])
labels = [
item.get('toString', '')
for item in items
if item.get('field') == 'labels' and 'toString' in item
]
if 'openhands' not in labels:
return None
issue_data = payload.get('issue', {})
issue_id = issue_data.get('id')
issue_key = issue_data.get('key')
base_api_url = issue_data.get('self', '').split('/rest/')[0]
user_data = payload.get('user', {})
user_email = user_data.get('emailAddress')
display_name = user_data.get('displayName')
account_id = user_data.get('accountId')
comment = ''
else:
return None
workspace_name = ''
parsedUrl = urlparse(base_api_url)
if parsedUrl.hostname:
workspace_name = parsedUrl.hostname
if not all(
[
issue_id,
issue_key,
user_email,
display_name,
account_id,
workspace_name,
base_api_url,
]
):
return None
return JobContext(
issue_id=issue_id,
issue_key=issue_key,
user_msg=comment,
user_email=user_email,
display_name=display_name,
platform_user_id=account_id,
workspace_name=workspace_name,
base_api_url=base_api_url,
)
async def receive_message(self, message: Message):
"""Process incoming Jira webhook message."""
payload = message.message.get('payload', {})
job_context = self.parse_webhook(payload)
if not job_context:
logger.info('[Jira] 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'[Jira] No workspace found for email domain: {job_context.user_email}'
)
await self._send_error_comment(
job_context,
'Your workspace is not configured with Jira 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'[Jira] Workspace {workspace.id} is not active')
await self._send_error_comment(
job_context,
'Jira integration is not active for your workspace.',
workspace,
)
return
# Authenticate user
jira_user, saas_user_auth = await self.authenticate_user(
job_context.platform_user_id, workspace.id
)
if not jira_user or not saas_user_auth:
logger.warning(
f'[Jira] User authentication failed for {job_context.user_email}'
)
await self._send_error_comment(
job_context,
f'User {job_context.user_email} is not authenticated or active in the Jira 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, workspace.jira_cloud_id, workspace.svc_acc_email, api_key
)
job_context.issue_title = issue_title
job_context.issue_description = issue_description
except Exception as e:
logger.error(f'[Jira] Failed to get issue context: {str(e)}')
await self._send_error_comment(
job_context,
'Failed to retrieve issue details. Please check the issue key and try again.',
workspace,
)
return
try:
# Create Jira view
jira_view = await JiraFactory.create_jira_view_from_payload(
job_context,
saas_user_auth,
jira_user,
workspace,
)
except Exception as e:
logger.error(f'[Jira] Failed to create jira view: {str(e)}', exc_info=True)
await self._send_error_comment(
job_context,
'Failed to initialize conversation. Please try again.',
workspace,
)
return
if not await self.is_job_requested(message, jira_view):
return
await self.start_job(jira_view)
async def is_job_requested(
self, message: Message, jira_view: JiraViewInterface
) -> bool:
"""
Check if a job is requested and handle repository selection.
"""
if isinstance(jira_view, JiraExistingConversationView):
return True
try:
# Get user repositories
user_repos: list[Repository] = await self._get_repositories(
jira_view.saas_user_auth
)
target_str = f'{jira_view.job_context.issue_description}\n{jira_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
jira_view.selected_repo = repos[0].full_name
logger.info(f'[Jira] Inferred repository: {repos[0].full_name}')
return True
else:
# No clear match - send repository selection comment
await self._send_repo_selection_comment(jira_view)
return False
except Exception as e:
logger.error(f'[Jira] Error in is_job_requested: {str(e)}')
return False
async def start_job(self, jira_view: JiraViewInterface):
async def start_job(self, view: JiraViewInterface):
"""Start a Jira job/conversation."""
# Import here to prevent circular import
from server.conversation_callback_processor.jira_callback_processor import (
@@ -339,97 +265,79 @@ class JiraManager(Manager):
)
try:
user_info: JiraUser = jira_view.jira_user
logger.info(
f'[Jira] Starting job for user {user_info.keycloak_user_id} '
f'issue {jira_view.job_context.issue_key}',
'[Jira] Starting job',
extra={
'issue_key': view.payload.issue_key,
'user_id': view.jira_user.keycloak_user_id,
'selected_repo': view.selected_repo,
},
)
# Create conversation
conversation_id = await jira_view.create_or_update_conversation(
self.jinja_env
)
conversation_id = await view.create_or_update_conversation(self.jinja_env)
logger.info(
f'[Jira] Created/Updated conversation {conversation_id} for issue {jira_view.job_context.issue_key}'
'[Jira] Conversation created',
extra={
'conversation_id': conversation_id,
'issue_key': view.payload.issue_key,
},
)
# Register callback processor for updates
if isinstance(jira_view, JiraNewConversationView):
if isinstance(view, JiraNewConversationView):
processor = JiraCallbackProcessor(
issue_key=jira_view.job_context.issue_key,
workspace_name=jira_view.jira_workspace.name,
issue_key=view.payload.issue_key,
workspace_name=view.jira_workspace.name,
)
# Register the callback processor
register_callback_processor(conversation_id, processor)
logger.info(
f'[Jira] Created callback processor for conversation {conversation_id}'
'[Jira] Callback processor registered',
extra={'conversation_id': conversation_id},
)
# Send initial response
msg_info = jira_view.get_response_msg()
# Send success response
msg_info = view.get_response_msg()
except MissingSettingsError as e:
logger.warning(f'[Jira] Missing settings error: {str(e)}')
logger.warning(
'[Jira] Missing settings error',
extra={'issue_key': view.payload.issue_key, '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'[Jira] LLM authentication error: {str(e)}')
logger.warning(
'[Jira] LLM authentication error',
extra={'issue_key': view.payload.issue_key, '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(
'[Jira] Session expired',
extra={'issue_key': view.payload.issue_key, 'error': str(e)},
)
msg_info = get_session_expired_message()
except StartingConvoException as e:
logger.warning(
'[Jira] Conversation start failed',
extra={'issue_key': view.payload.issue_key, 'error': str(e)},
)
msg_info = str(e)
except Exception as e:
logger.error(
f'[Jira] Unexpected error starting job: {str(e)}', exc_info=True
'[Jira] Unexpected error starting job',
extra={'issue_key': view.payload.issue_key, 'error': 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(
jira_view.jira_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(msg=msg_info),
issue_key=jira_view.job_context.issue_key,
jira_cloud_id=jira_view.jira_workspace.jira_cloud_id,
svc_acc_email=jira_view.jira_workspace.svc_acc_email,
svc_acc_api_key=api_key,
)
except Exception as e:
logger.error(f'[Jira] Failed to send response message: {str(e)}')
async def get_issue_details(
self,
job_context: JobContext,
jira_cloud_id: str,
svc_acc_email: str,
svc_acc_api_key: str,
) -> Tuple[str, str]:
url = f'{JIRA_CLOUD_API_URL}/{jira_cloud_id}/rest/api/2/issue/{job_context.issue_key}'
async with httpx.AsyncClient() as client:
response = await client.get(url, auth=(svc_acc_email, svc_acc_api_key))
response.raise_for_status()
issue_payload = response.json()
if not issue_payload:
raise ValueError(f'Issue with key {job_context.issue_key} not found.')
title = issue_payload.get('fields', {}).get('summary', '')
description = issue_payload.get('fields', {}).get('description', '')
if not title:
raise ValueError(
f'Issue with key {job_context.issue_key} does not have a title.'
)
if not description:
raise ValueError(
f'Issue with key {job_context.issue_key} does not have a description.'
)
return title, description
await self._send_comment(view, msg_info)
async def send_message(
self,
@@ -439,65 +347,65 @@ class JiraManager(Manager):
svc_acc_email: str,
svc_acc_api_key: str,
):
"""Send a comment to a Jira issue."""
url = (
f'{JIRA_CLOUD_API_URL}/{jira_cloud_id}/rest/api/2/issue/{issue_key}/comment'
)
data = {'body': message.message}
async with httpx.AsyncClient() as client:
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
)
response.raise_for_status()
return response.json()
async def _send_error_comment(
self,
job_context: JobContext,
error_msg: str,
workspace: JiraWorkspace | None,
):
"""Send error comment to Jira issue."""
if not workspace:
logger.error('[Jira] Cannot send error comment - no workspace available')
return
async def _send_comment(self, view: JiraViewInterface, msg: str):
"""Send a comment using credentials from the view."""
try:
api_key = self.token_manager.decrypt_text(
view.jira_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(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,
svc_acc_api_key=api_key,
)
except Exception as e:
logger.error(
'[Jira] Failed to send comment',
extra={'issue_key': view.payload.issue_key, 'error': str(e)},
)
async def _send_error_from_payload(
self,
payload: JiraWebhookPayload,
workspace: JiraWorkspace,
error_msg: str,
):
"""Send error comment before view is created (using payload directly)."""
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_key=job_context.issue_key,
issue_key=payload.issue_key,
jira_cloud_id=workspace.jira_cloud_id,
svc_acc_email=workspace.svc_acc_email,
svc_acc_api_key=api_key,
)
except Exception as e:
logger.error(f'[Jira] Failed to send error comment: {str(e)}')
async def _send_repo_selection_comment(self, jira_view: JiraViewInterface):
"""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(
jira_view.jira_workspace.svc_acc_api_key
)
await self.send_message(
self.create_outgoing_message(msg=comment_msg),
issue_key=jira_view.job_context.issue_key,
jira_cloud_id=jira_view.jira_workspace.jira_cloud_id,
svc_acc_email=jira_view.jira_workspace.svc_acc_email,
svc_acc_api_key=api_key,
)
logger.info(
f'[Jira] Sent repository selection comment for issue {jira_view.job_context.issue_key}'
)
except Exception as e:
logger.error(
f'[Jira] Failed to send repository selection comment: {str(e)}'
'[Jira] Failed to send error comment',
extra={'issue_key': payload.issue_key, 'error': str(e)},
)
def get_workspace_name_from_payload(self, payload: dict) -> str | None:
"""Extract workspace name from Jira webhook payload.
This method is used by the route for signature verification.
"""
parse_result = self.payload_parser.parse(payload)
if isinstance(parse_result, JiraPayloadSuccess):
return parse_result.payload.workspace_name
return None

View File

@@ -0,0 +1,267 @@
"""Centralized payload parsing for Jira webhooks.
This module provides a single source of truth for parsing and validating
Jira webhook payloads, replacing scattered parsing logic throughout the codebase.
"""
from dataclasses import dataclass
from enum import Enum
from urllib.parse import urlparse
from openhands.core.logger import openhands_logger as logger
class JiraEventType(Enum):
"""Types of Jira events we handle."""
LABELED_TICKET = 'labeled_ticket'
COMMENT_MENTION = 'comment_mention'
@dataclass(frozen=True)
class JiraWebhookPayload:
"""Normalized, validated representation of a Jira webhook payload.
This immutable dataclass replaces JobContext and provides a single
source of truth for all webhook data. All parsing happens in
JiraPayloadParser, ensuring consistent validation.
"""
event_type: JiraEventType
raw_event: str # Original webhookEvent value
# Issue data
issue_id: str
issue_key: str
# User data
user_email: str
display_name: str
account_id: str
# Workspace data (derived from issue self URL)
workspace_name: str
base_api_url: str
# Event-specific data
comment_body: str = '' # For comment events
@property
def user_msg(self) -> str:
"""Alias for comment_body for backward compatibility."""
return self.comment_body
class JiraPayloadParseError(Exception):
"""Raised when payload parsing fails."""
def __init__(self, reason: str, event_type: str | None = None):
self.reason = reason
self.event_type = event_type
super().__init__(reason)
@dataclass(frozen=True)
class JiraPayloadSuccess:
"""Result when parsing succeeds."""
payload: JiraWebhookPayload
@dataclass(frozen=True)
class JiraPayloadSkipped:
"""Result when event is intentionally skipped."""
skip_reason: str
@dataclass(frozen=True)
class JiraPayloadError:
"""Result when parsing fails due to invalid data."""
error: str
JiraPayloadParseResult = JiraPayloadSuccess | JiraPayloadSkipped | JiraPayloadError
class JiraPayloadParser:
"""Centralized parser for Jira webhook payloads.
This class provides a single entry point for parsing webhooks,
determining event types, and extracting all necessary fields.
Replaces scattered parsing in JiraFactory and JiraManager.
"""
def __init__(self, oh_label: str, inline_oh_label: str):
"""Initialize parser with OpenHands label configuration.
Args:
oh_label: Label that triggers OpenHands (e.g., 'openhands')
inline_oh_label: Mention that triggers OpenHands (e.g., '@openhands')
"""
self.oh_label = oh_label
self.inline_oh_label = inline_oh_label
def parse(self, raw_payload: dict) -> JiraPayloadParseResult:
"""Parse a raw webhook payload into a normalized JiraWebhookPayload.
Args:
raw_payload: The raw webhook payload dict from Jira
Returns:
One of:
- JiraPayloadSuccess: Valid, actionable event with payload
- JiraPayloadSkipped: Event we intentionally don't process
- JiraPayloadError: Malformed payload we expected to process
"""
webhook_event = raw_payload.get('webhookEvent', '')
logger.debug(
'[Jira] Parsing webhook payload', extra={'webhook_event': webhook_event}
)
if webhook_event == 'jira:issue_updated':
return self._parse_label_event(raw_payload, webhook_event)
elif webhook_event == 'comment_created':
return self._parse_comment_event(raw_payload, webhook_event)
else:
return JiraPayloadSkipped(f'Unhandled webhook event type: {webhook_event}')
def _parse_label_event(
self, payload: dict, webhook_event: str
) -> JiraPayloadParseResult:
"""Parse an issue_updated event for label changes."""
changelog = payload.get('changelog', {})
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
]
if self.oh_label not in labels:
return JiraPayloadSkipped(
f"Label event does not contain '{self.oh_label}' label"
)
# For label events, user data comes from 'user' field
user_data = payload.get('user', {})
return self._extract_and_validate(
payload=payload,
user_data=user_data,
event_type=JiraEventType.LABELED_TICKET,
webhook_event=webhook_event,
comment_body='',
)
def _parse_comment_event(
self, payload: dict, webhook_event: str
) -> JiraPayloadParseResult:
"""Parse a comment_created event."""
comment_data = payload.get('comment', {})
comment_body = comment_data.get('body', '')
if not self._has_mention(comment_body):
return JiraPayloadSkipped(
f"Comment does not mention '{self.inline_oh_label}'"
)
# For comment events, user data comes from 'comment.author'
user_data = comment_data.get('author', {})
return self._extract_and_validate(
payload=payload,
user_data=user_data,
event_type=JiraEventType.COMMENT_MENTION,
webhook_event=webhook_event,
comment_body=comment_body,
)
def _has_mention(self, text: str) -> bool:
"""Check if text contains an exact mention of OpenHands."""
from integrations.utils import has_exact_mention
return has_exact_mention(text, self.inline_oh_label)
def _extract_and_validate(
self,
payload: dict,
user_data: dict,
event_type: JiraEventType,
webhook_event: str,
comment_body: str,
) -> JiraPayloadParseResult:
"""Extract common fields and validate required data is present."""
issue_data = payload.get('issue', {})
# Extract all fields with empty string defaults (makes them str type)
issue_id = issue_data.get('id', '')
issue_key = issue_data.get('key', '')
user_email = user_data.get('emailAddress', '')
display_name = user_data.get('displayName', '')
account_id = user_data.get('accountId', '')
base_api_url, workspace_name = self._extract_workspace_from_url(
issue_data.get('self', '')
)
# Validate required fields
missing: list[str] = []
if not issue_id:
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:
missing.append('user.accountId')
if not workspace_name:
missing.append('workspace_name (derived from issue.self)')
if not base_api_url:
missing.append('base_api_url (derived from issue.self)')
if missing:
return JiraPayloadError(f"Missing required fields: {', '.join(missing)}")
return JiraPayloadSuccess(
JiraWebhookPayload(
event_type=event_type,
raw_event=webhook_event,
issue_id=issue_id,
issue_key=issue_key,
user_email=user_email,
display_name=display_name,
account_id=account_id,
workspace_name=workspace_name,
base_api_url=base_api_url,
comment_body=comment_body,
)
)
def _extract_workspace_from_url(self, self_url: str) -> tuple[str, str]:
"""Extract base API URL and workspace name from issue self URL.
Args:
self_url: The 'self' URL from the issue data
Returns:
Tuple of (base_api_url, workspace_name)
"""
if not self_url:
return '', ''
# Extract base URL (everything before /rest/)
if '/rest/' in self_url:
base_api_url = self_url.split('/rest/')[0]
else:
parsed = urlparse(self_url)
base_api_url = f'{parsed.scheme}://{parsed.netloc}'
# Extract workspace name (hostname)
parsed = urlparse(base_api_url)
workspace_name = parsed.hostname or ''
return base_api_url, workspace_name

View File

@@ -1,26 +1,42 @@
from abc import ABC, abstractmethod
"""Type definitions and interfaces for Jira integration."""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from integrations.models import JobContext
from jinja2 import Environment
from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
from openhands.server.user_auth.user_auth import UserAuth
if TYPE_CHECKING:
from integrations.jira.jira_payload import JiraWebhookPayload
class JiraViewInterface(ABC):
"""Interface for Jira views that handle different types of Jira interactions."""
"""Interface for Jira views that handle different types of Jira interactions.
job_context: JobContext
Views hold the webhook payload directly rather than duplicating fields,
and fetch issue details lazily when needed.
"""
# Core data - view holds these references
payload: 'JiraWebhookPayload'
saas_user_auth: UserAuth
jira_user: JiraUser
jira_workspace: JiraWorkspace
# Mutable state set during processing
selected_repo: str | None
conversation_id: str
@abstractmethod
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Get initial instructions for the conversation."""
async def get_issue_details(self) -> tuple[str, str]:
"""Fetch and cache issue title and description from Jira API.
Returns:
Tuple of (issue_title, issue_description)
"""
pass
@abstractmethod
@@ -35,6 +51,21 @@ class JiraViewInterface(ABC):
class StartingConvoException(Exception):
"""Exception raised when starting a conversation fails."""
"""Exception raised when starting a conversation fails.
This provides user-friendly error messages that can be sent back to Jira.
"""
pass
class RepositoryNotFoundError(Exception):
"""Raised when a repository cannot be determined from the issue.
This is a separate error domain from StartingConvoException - it represents
a precondition failure (no repo configured/found) rather than a conversation
creation failure. The manager catches this and converts it to a user-friendly
message.
"""
pass

View File

@@ -1,8 +1,21 @@
from dataclasses import dataclass
"""Jira view implementations and factory.
from integrations.jira.jira_types import JiraViewInterface, StartingConvoException
from integrations.models import JobContext
from integrations.utils import CONVERSATION_URL, get_final_agent_observation
Views are responsible for:
- Holding the webhook payload and auth context
- Lazy-loading issue details from Jira API when needed
- Creating conversations with the selected repository
"""
from dataclasses import dataclass, field
import httpx
from integrations.jira.jira_payload import JiraWebhookPayload
from integrations.jira.jira_types import (
JiraViewInterface,
RepositoryNotFoundError,
StartingConvoException,
)
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
@@ -10,55 +23,147 @@ from storage.jira_user import JiraUser
from storage.jira_workspace import JiraWorkspace
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.server.shared import ConversationStoreImpl, config, conversation_manager
from openhands.integrations.provider import ProviderHandler
from openhands.server.services.conversation_service import create_new_conversation
from openhands.server.user_auth.user_auth import UserAuth
from openhands.storage.data_models.conversation_metadata import ConversationTrigger
from openhands.utils.http_session import httpx_verify_option
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
integration_store = JiraIntegrationStore.get_instance()
@dataclass
class JiraNewConversationView(JiraViewInterface):
job_context: JobContext
"""View for creating a new Jira conversation.
This view holds the webhook payload directly and lazily fetches
issue details when needed for rendering templates.
"""
payload: JiraWebhookPayload
saas_user_auth: UserAuth
jira_user: JiraUser
jira_workspace: JiraWorkspace
selected_repo: str | None
conversation_id: str
selected_repo: str | None = None
conversation_id: str = ''
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
# Lazy-loaded issue details (cached after first fetch)
_issue_title: str | None = field(default=None, repr=False)
_issue_description: str | None = field(default=None, repr=False)
# Decrypted API key (set by factory)
_decrypted_api_key: str = field(default='', repr=False)
async def get_issue_details(self) -> tuple[str, str]:
"""Fetch issue details from Jira API (cached after first call).
Returns:
Tuple of (issue_title, issue_description)
Raises:
StartingConvoException: If issue details cannot be fetched
"""
if self._issue_title is not None and self._issue_description is not None:
return self._issue_title, self._issue_description
try:
url = f'{JIRA_CLOUD_API_URL}/{self.jira_workspace.jira_cloud_id}/rest/api/2/issue/{self.payload.issue_key}'
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.get(
url,
auth=(
self.jira_workspace.svc_acc_email,
self._decrypted_api_key,
),
)
response.raise_for_status()
issue_payload = response.json()
if not issue_payload:
raise StartingConvoException(
f'Issue {self.payload.issue_key} not found.'
)
self._issue_title = issue_payload.get('fields', {}).get('summary', '')
self._issue_description = (
issue_payload.get('fields', {}).get('description', '') or ''
)
if not self._issue_title:
raise StartingConvoException(
f'Issue {self.payload.issue_key} does not have a title.'
)
logger.info(
'[Jira] Fetched issue details',
extra={
'issue_key': self.payload.issue_key,
'has_description': bool(self._issue_description),
},
)
return self._issue_title, self._issue_description
except httpx.HTTPStatusError as e:
logger.error(
'[Jira] Failed to fetch issue details',
extra={
'issue_key': self.payload.issue_key,
'status': e.response.status_code,
},
)
raise StartingConvoException(
f'Failed to fetch issue details: HTTP {e.response.status_code}'
)
except Exception as e:
if isinstance(e, StartingConvoException):
raise
logger.error(
'[Jira] Failed to fetch issue details',
extra={'issue_key': self.payload.issue_key, 'error': str(e)},
)
raise StartingConvoException(f'Failed to fetch issue details: {str(e)}')
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Get instructions for the conversation.
This fetches issue details if not already cached.
Returns:
Tuple of (system_instructions, user_message)
"""
issue_title, issue_description = await self.get_issue_details()
instructions_template = jinja_env.get_template('jira_instructions.j2')
instructions = instructions_template.render()
user_msg_template = jinja_env.get_template('jira_new_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.job_context.issue_key,
issue_title=self.job_context.issue_title,
issue_description=self.job_context.issue_description,
user_message=self.job_context.user_msg or '',
issue_key=self.payload.issue_key,
issue_title=issue_title,
issue_description=issue_description,
user_message=self.payload.user_msg,
)
return instructions, user_msg
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Create a new Jira conversation"""
"""Create a new Jira conversation.
Returns:
The conversation ID
Raises:
StartingConvoException: If conversation creation fails
"""
if not self.selected_repo:
raise StartingConvoException('No repository selected for this conversation')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = await self._get_instructions(jinja_env)
try:
agent_loop_info = await create_new_conversation(
@@ -76,147 +181,259 @@ class JiraNewConversationView(JiraViewInterface):
self.conversation_id = agent_loop_info.conversation_id
logger.info(f'[Jira] Created conversation {self.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.job_context.issue_id,
issue_key=self.job_context.issue_key,
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
except Exception as e:
if isinstance(e, StartingConvoException):
raise
logger.error(
f'[Jira] Failed to create conversation: {str(e)}', exc_info=True
'[Jira] Failed to create conversation',
extra={'issue_key': self.payload.issue_key, 'error': str(e)},
exc_info=True,
)
raise StartingConvoException(f'Failed to create conversation: {str(e)}')
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 [track my progress here|{conversation_link}]."
@dataclass
class JiraExistingConversationView(JiraViewInterface):
job_context: JobContext
saas_user_auth: UserAuth
jira_user: JiraUser
jira_workspace: JiraWorkspace
selected_repo: str | None
conversation_id: str
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"""Instructions passed when conversation is first initialized"""
user_msg_template = jinja_env.get_template('jira_existing_conversation.j2')
user_msg = user_msg_template.render(
issue_key=self.job_context.issue_key,
user_message=self.job_context.user_msg or '',
issue_title=self.job_context.issue_title,
issue_description=self.job_context.issue_description,
)
return '', user_msg
async def create_or_update_conversation(self, jinja_env: Environment) -> str:
"""Update an existing Jira conversation"""
user_id = self.jira_user.keycloak_user_id
try:
conversation_store = await ConversationStoreImpl.get_instance(
config, user_id
)
metadata = await conversation_store.get_metadata(self.conversation_id)
if not metadata:
raise StartingConvoException('Conversation no longer exists.')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
# Should we raise here if there are no providers?
providers_set = list(provider_tokens.keys()) if provider_tokens else []
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)}')
def get_response_msg(self) -> str:
"""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}]."
return f"I'm on it! {self.payload.display_name} can [track my progress here|{conversation_link}]."
class JiraFactory:
"""Factory for creating Jira views based on message content"""
"""Factory for creating Jira views.
The factory is responsible for:
- Creating the appropriate view type
- Inferring and selecting the repository
- Validating all required data is available
Repository selection happens here so that view creation either
succeeds with a valid repo or fails with a clear error.
"""
@staticmethod
async def create_jira_view_from_payload(
job_context: JobContext,
saas_user_auth: UserAuth,
jira_user: JiraUser,
jira_workspace: JiraWorkspace,
async def _create_provider_handler(user_auth: UserAuth) -> ProviderHandler | None:
"""Create a ProviderHandler for the user."""
provider_tokens = await user_auth.get_provider_tokens()
if provider_tokens is None:
return None
access_token = await user_auth.get_access_token()
user_id = await user_auth.get_user_id()
return ProviderHandler(
provider_tokens=provider_tokens,
external_auth_token=access_token,
external_auth_id=user_id,
)
@staticmethod
def _extract_potential_repos(
issue_key: str,
issue_title: str,
issue_description: str,
user_msg: str,
) -> list[str]:
"""Extract potential repository names from issue content.
Raises:
RepositoryNotFoundError: If no potential repos found in text.
"""
search_text = f'{issue_title}\n{issue_description}\n{user_msg}'
potential_repos = infer_repo_from_message(search_text)
if not potential_repos:
raise RepositoryNotFoundError(
'Could not determine which repository to use. '
'Please mention the repository (e.g., owner/repo) in the issue description or comment.'
)
logger.info(
'[Jira] Found potential repositories in issue content',
extra={'issue_key': issue_key, 'potential_repos': potential_repos},
)
return potential_repos
@staticmethod
async def _verify_repos(
issue_key: str,
potential_repos: list[str],
provider_handler: ProviderHandler,
) -> list[str]:
"""Verify which repos the user has access to."""
verified_repos: list[str] = []
for repo_name in potential_repos:
try:
repository = await provider_handler.verify_repo_provider(repo_name)
verified_repos.append(repository.full_name)
logger.debug(
'[Jira] Repository verification succeeded',
extra={'issue_key': issue_key, 'repository': repository.full_name},
)
except Exception as e:
logger.debug(
'[Jira] Repository verification failed',
extra={
'issue_key': issue_key,
'repo_name': repo_name,
'error': str(e),
},
)
return verified_repos
@staticmethod
def _select_single_repo(
issue_key: str,
potential_repos: list[str],
verified_repos: list[str],
) -> str:
"""Select exactly one repo from verified repos.
Raises:
RepositoryNotFoundError: If zero or multiple repos verified.
"""
if len(verified_repos) == 0:
raise RepositoryNotFoundError(
f'Could not access any of the mentioned repositories: {", ".join(potential_repos)}. '
'Please ensure you have access to the repository and it exists.'
)
if len(verified_repos) > 1:
raise RepositoryNotFoundError(
f'Multiple repositories found: {", ".join(verified_repos)}. '
'Please specify exactly one repository in the issue description or comment.'
)
logger.info(
'[Jira] Verified repository access',
extra={'issue_key': issue_key, 'repository': verified_repos[0]},
)
return verified_repos[0]
@staticmethod
async def _infer_repository(
payload: JiraWebhookPayload,
user_auth: UserAuth,
issue_title: str,
issue_description: str,
) -> str:
"""Infer and verify the repository from issue content.
Raises:
RepositoryNotFoundError: If no valid repository can be determined.
"""
provider_handler = await JiraFactory._create_provider_handler(user_auth)
if not provider_handler:
raise RepositoryNotFoundError(
'No Git provider connected. Please connect a Git provider in OpenHands settings.'
)
potential_repos = JiraFactory._extract_potential_repos(
payload.issue_key, issue_title, issue_description, payload.user_msg
)
verified_repos = await JiraFactory._verify_repos(
payload.issue_key, potential_repos, provider_handler
)
return JiraFactory._select_single_repo(
payload.issue_key, potential_repos, verified_repos
)
@staticmethod
async def create_view(
payload: JiraWebhookPayload,
workspace: JiraWorkspace,
user: JiraUser,
user_auth: UserAuth,
decrypted_api_key: str,
) -> JiraViewInterface:
"""Create appropriate Jira view based on the message and user state"""
"""Create a Jira view with repository already selected.
if not jira_user or not saas_user_auth or not jira_workspace:
raise StartingConvoException('User not authenticated with Jira integration')
This factory method:
1. Creates the view with payload and auth context
2. Fetches issue details (needed for repo inference)
3. Infers and selects the repository
conversation = await integration_store.get_user_conversations_by_issue_id(
job_context.issue_id, jira_user.id
If any step fails, an appropriate exception is raised with
a user-friendly message.
Args:
payload: Parsed webhook payload
workspace: The Jira workspace
user: The Jira user
user_auth: OpenHands user authentication
decrypted_api_key: Decrypted service account API key
Returns:
A JiraViewInterface with selected_repo populated
Raises:
StartingConvoException: If view creation fails
RepositoryNotFoundError: If repository cannot be determined
"""
logger.info(
'[Jira] Creating view',
extra={
'issue_key': payload.issue_key,
'event_type': payload.event_type.value,
},
)
if conversation:
logger.info(
f'[Jira] Found existing conversation for issue {job_context.issue_id}'
)
return JiraExistingConversationView(
job_context=job_context,
saas_user_auth=saas_user_auth,
jira_user=jira_user,
jira_workspace=jira_workspace,
selected_repo=None,
conversation_id=conversation.conversation_id,
)
return JiraNewConversationView(
job_context=job_context,
saas_user_auth=saas_user_auth,
jira_user=jira_user,
jira_workspace=jira_workspace,
selected_repo=None, # Will be set later after repo inference
conversation_id='', # Will be set when conversation is created
# Create the view
view = JiraNewConversationView(
payload=payload,
saas_user_auth=user_auth,
jira_user=user,
jira_workspace=workspace,
_decrypted_api_key=decrypted_api_key,
)
# Fetch issue details (needed for repo inference)
try:
issue_title, issue_description = await view.get_issue_details()
except StartingConvoException:
raise # Re-raise with original message
except Exception as e:
raise StartingConvoException(f'Failed to fetch issue details: {str(e)}')
# Infer and select repository
selected_repo = await JiraFactory._infer_repository(
payload=payload,
user_auth=user_auth,
issue_title=issue_title,
issue_description=issue_description,
)
view.selected_repo = selected_repo
logger.info(
'[Jira] View created successfully',
extra={
'issue_key': payload.issue_key,
'selected_repo': selected_repo,
},
)
return view

View File

@@ -19,6 +19,7 @@ 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
@@ -32,8 +33,13 @@ 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
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 JiraDcManager(Manager):
@@ -396,6 +402,10 @@ class JiraDcManager(Manager):
logger.warning(f'[Jira DC] 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'[Jira DC] Session expired: {str(e)}')
msg_info = get_session_expired_message()
except Exception as e:
logger.error(
f'[Jira DC] Unexpected error starting job: {str(e)}', exc_info=True
@@ -422,7 +432,7 @@ class JiraDcManager(Manager):
"""Get issue details from Jira DC API."""
url = f'{job_context.base_api_url}/rest/api/2/issue/{job_context.issue_key}'
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
issue_payload = response.json()
@@ -452,7 +462,7 @@ class JiraDcManager(Manager):
url = f'{base_api_url}/rest/api/2/issue/{issue_key}/comment'
headers = {'Authorization': f'Bearer {svc_acc_api_key}'}
data = {'body': message.message}
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()

View File

@@ -60,7 +60,7 @@ class JiraDcNewConversationView(JiraDcViewInterface):
raise StartingConvoException('No repository selected for this conversation')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
try:
@@ -135,8 +135,10 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
conversation_store = await ConversationStoreImpl.get_instance(
config, user_id
)
metadata = await conversation_store.get_metadata(self.conversation_id)
if not metadata:
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()

View File

@@ -16,6 +16,7 @@ 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
@@ -29,8 +30,13 @@ 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
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):
@@ -386,6 +392,10 @@ class LinearManager(Manager):
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
@@ -408,7 +418,7 @@ class LinearManager(Manager):
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() as client:
async with httpx.AsyncClient(verify=httpx_verify_option()) as client:
response = await client.post(
self.api_url,
headers=headers,

View File

@@ -57,7 +57,7 @@ class LinearNewConversationView(LinearViewInterface):
raise StartingConvoException('No repository selected for this conversation')
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
instructions, user_msg = self._get_instructions(jinja_env)
try:
@@ -132,8 +132,10 @@ class LinearExistingConversationView(LinearViewInterface):
conversation_store = await ConversationStoreImpl.get_instance(
config, user_id
)
metadata = await conversation_store.get_metadata(self.conversation_id)
if not metadata:
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()

View File

@@ -16,11 +16,6 @@ class Manager(ABC):
"Send message to integration from Openhands server"
raise NotImplementedError
@abstractmethod
async def is_job_requested(self, message: Message) -> bool:
"Confirm that a job is being requested"
raise NotImplementedError
@abstractmethod
def start_job(self):
"Kick off a job with openhands agent"

View File

@@ -0,0 +1,66 @@
from openhands.app_server.user.user_context import UserContext
from openhands.app_server.user.user_models import UserInfo
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.integrations.service_types import ProviderType
from openhands.sdk.secret import SecretSource, StaticSecret
from openhands.server.user_auth.user_auth import UserAuth
class ResolverUserContext(UserContext):
"""User context for resolver operations that inherits from UserContext."""
def __init__(
self,
saas_user_auth: UserAuth,
):
self.saas_user_auth = saas_user_auth
async def get_user_id(self) -> str | None:
return await self.saas_user_auth.get_user_id()
async def get_user_info(self) -> UserInfo:
user_settings = await self.saas_user_auth.get_user_settings()
user_id = await self.saas_user_auth.get_user_id()
if user_settings:
return UserInfo(
id=user_id,
**user_settings.model_dump(context={'expose_secrets': True}),
)
return UserInfo(id=user_id)
async def get_authenticated_git_url(
self, repository: str, is_optional: bool = False
) -> str:
# This would need to be implemented based on the git provider tokens
# For now, return a basic HTTPS URL
return f'https://github.com/{repository}.git'
async def get_latest_token(self, provider_type: ProviderType) -> str | None:
# Return the appropriate token string from git_provider_tokens
provider_tokens = await self.saas_user_auth.get_provider_tokens()
if provider_tokens:
provider_token = provider_tokens.get(provider_type)
if provider_token and provider_token.token:
return provider_token.token.get_secret_value()
return None
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
return await self.saas_user_auth.get_provider_tokens()
async def get_secrets(self) -> dict[str, SecretSource]:
"""Get secrets for the user, including custom secrets."""
secrets = await self.saas_user_auth.get_secrets()
if secrets:
# Convert custom secrets to StaticSecret objects for SDK compatibility
# secrets.custom_secrets is of type Mapping[str, CustomSecret]
converted_secrets = {}
for key, custom_secret in secrets.custom_secrets.items():
# Extract the secret value from CustomSecret and convert to StaticSecret
secret_value = custom_secret.secret.get_secret_value()
converted_secrets[key] = StaticSecret(value=secret_value)
return converted_secrets
return {}
async def get_mcp_api_key(self) -> str | None:
return await self.saas_user_auth.get_mcp_api_key()

View File

@@ -14,10 +14,10 @@ from integrations.slack.slack_view import (
from integrations.utils import (
HOST_URL,
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.saas_user_auth import SaasUserAuth
from server.constants import SLACK_CLIENT_ID
from server.utils.conversation_callback_utils import register_callback_processor
from slack_sdk.oauth import AuthorizeUrlGenerator
@@ -29,7 +29,11 @@ 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 config, server_config
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.server.types import (
LLMAuthenticationError,
MissingSettingsError,
SessionExpiredError,
)
from openhands.server.user_auth.user_auth import UserAuth
authorize_url_generator = AuthorizeUrlGenerator(
@@ -54,17 +58,6 @@ class SlackManager(Manager):
if message.source != SourceType.SLACK:
raise ValueError(f'Unexpected message source {message.source}')
async def _get_user_auth(self, keycloak_user_id: str) -> UserAuth:
offline_token = await self.token_manager.load_offline_token(keycloak_user_id)
if offline_token is None:
logger.info('no_offline_token_found')
user_auth = SaasUserAuth(
user_id=keycloak_user_id,
refresh_token=SecretStr(offline_token),
)
return user_auth
async def authenticate_user(
self, slack_user_id: str
) -> tuple[SlackUser | None, UserAuth | None]:
@@ -81,13 +74,15 @@ class SlackManager(Manager):
saas_user_auth = None
if slack_user:
saas_user_auth = await self._get_user_auth(slack_user.keycloak_user_id)
saas_user_auth = await get_saas_user_auth(
slack_user.keycloak_user_id, self.token_manager
)
# slack_view.saas_user_auth = await self._get_user_auth(slack_view.slack_to_openhands_user.keycloak_user_id)
return slack_user, saas_user_auth
def _infer_repo_from_message(self, user_msg: str) -> str | None:
# Regular expression to match patterns like "All-Hands-AI/OpenHands" or "deploy repo"
# Regular expression to match patterns like "OpenHands/OpenHands" or "deploy repo"
pattern = r'([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)|([a-zA-Z0-9_-]+)(?=\s+repo)'
match = re.search(pattern, user_msg)
@@ -244,13 +239,11 @@ class SlackManager(Manager):
async def is_job_requested(
self, message: Message, slack_view: SlackViewInterface
) -> bool:
"""
A job is always request we only receive webhooks for events associated with the slack bot
"""A job is always request we only receive webhooks for events associated with the slack bot
This method really just checks
1. Is the user is authenticated
2. Do we have the necessary information to start a job (either by inferring the selected repo, otherwise asking the user)
"""
# Infer repo from user message is not needed; user selected repo from the form or is updating existing convo
if isinstance(slack_view, SlackUpdateExistingConversationView):
return True
@@ -317,10 +310,15 @@ class SlackManager(Manager):
f'[Slack] Created conversation {conversation_id} for user {user_info.slack_display_name}'
)
if not isinstance(slack_view, SlackUpdateExistingConversationView):
# Only add SlackCallbackProcessor for new conversations (not updates) and non-v1 conversations
if (
not isinstance(slack_view, SlackUpdateExistingConversationView)
and not slack_view.v1_enabled
):
# We don't re-subscribe for follow up messages from slack.
# Summaries are generated for every messages anyways, we only need to do
# this subscription once for the event which kicked off the job.
processor = SlackCallbackProcessor(
slack_user_id=slack_view.slack_user_id,
channel_id=slack_view.channel_id,
@@ -335,6 +333,14 @@ class SlackManager(Manager):
logger.info(
f'[Slack] Created callback processor for conversation {conversation_id}'
)
elif isinstance(slack_view, SlackUpdateExistingConversationView):
logger.info(
f'[Slack] Skipping callback processor for existing conversation update {conversation_id}'
)
elif slack_view.v1_enabled:
logger.info(
f'[Slack] Skipping callback processor for v1 conversation {conversation_id}'
)
msg_info = slack_view.get_response_msg()
@@ -352,6 +358,13 @@ class SlackManager(Manager):
msg_info = f'@{user_info.slack_display_name} please set a valid LLM API key in [OpenHands Cloud]({HOST_URL}) before starting a job.'
except SessionExpiredError as e:
logger.warning(
f'[Slack] Session expired for user {user_info.slack_display_name}: {str(e)}'
)
msg_info = get_session_expired_message(user_info.slack_display_name)
except StartingConvoException as e:
msg_info = str(e)

View File

@@ -21,20 +21,16 @@ class SlackViewInterface(SummaryExtractionTracker, ABC):
send_summary_instruction: bool
conversation_id: str
team_id: str
v1_enabled: bool
@abstractmethod
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"Instructions passed when conversation is first initialized"
"""Instructions passed when conversation is first initialized"""
pass
@abstractmethod
async def create_or_update_conversation(self, jinja_env: Environment):
"Create a new conversation"
pass
@abstractmethod
def get_callback_id(self) -> str:
"Unique callback id for subscribription made to EventStream for fetching agent summary"
"""Create a new conversation"""
pass
@abstractmethod
@@ -43,6 +39,4 @@ class SlackViewInterface(SummaryExtractionTracker, ABC):
class StartingConvoException(Exception):
"""
Raised when trying to send message to a conversation that's is still starting up
"""
"""Raised when trying to send message to a conversation that's is still starting up"""

View File

@@ -0,0 +1,273 @@
import logging
from uuid import UUID
import httpx
from integrations.utils import CONVERSATION_URL, get_summary_instruction
from pydantic import Field
from slack_sdk import WebClient
from storage.slack_team_store import SlackTeamStore
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 SlackV1CallbackProcessor(EventCallbackProcessor):
"""Callback processor for Slack V1 integrations."""
slack_view_data: dict[str, str | None] = Field(default_factory=dict)
async def __call__(
self,
conversation_id: UUID,
callback: EventCallback,
event: Event,
) -> EventCallbackResult | None:
"""Process events for Slack V1 integration."""
# Only handle ConversationStateUpdateEvent
if not isinstance(event, ConversationStateUpdateEvent):
return None
# Only act when execution has finished
if not (event.key == 'execution_status' and event.value == 'finished'):
return None
_logger.info('[Slack V1] Callback agent state was %s', event)
try:
summary = await self._request_summary(conversation_id)
await self._post_summary_to_slack(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('[Slack V1] Error processing callback: %s', e)
# Only try to post error to Slack if we have basic requirements
try:
await self._post_summary_to_slack(
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(
'[Slack V1] Failed to post error message to Slack: %s', post_error
)
return EventCallbackResult(
status=EventCallbackResultStatus.ERROR,
event_callback_id=callback.id,
event_id=event.id,
conversation_id=conversation_id,
detail=str(e),
)
# -------------------------------------------------------------------------
# Slack helpers
# -------------------------------------------------------------------------
def _get_bot_access_token(self):
slack_team_store = SlackTeamStore.get_instance()
bot_access_token = slack_team_store.get_team_bot_token(
self.slack_view_data['team_id']
)
return bot_access_token
async def _post_summary_to_slack(self, summary: str) -> None:
"""Post a summary message to the configured Slack channel."""
bot_access_token = self._get_bot_access_token()
if not bot_access_token:
raise RuntimeError('Missing Slack bot access token')
channel_id = self.slack_view_data['channel_id']
thread_ts = self.slack_view_data.get('thread_ts') or self.slack_view_data.get(
'message_ts'
)
client = WebClient(token=bot_access_token)
try:
# Post the summary as a threaded reply
response = client.chat_postMessage(
channel=channel_id,
text=summary,
thread_ts=thread_ts,
unfurl_links=False,
unfurl_media=False,
)
if not response['ok']:
raise RuntimeError(
f"Slack API error: {response.get('error', 'Unknown error')}"
)
_logger.info(
'[Slack V1] Successfully posted summary to channel %s', channel_id
)
except Exception as e:
_logger.error('[Slack V1] Failed to post message to Slack: %s', e)
raise
# -------------------------------------------------------------------------
# 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(
'[Slack 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(
'[Slack 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(
'[Slack 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,8 +1,16 @@
from dataclasses import dataclass
from uuid import UUID, uuid4
from integrations.models import Message
from integrations.resolver_context import ResolverUserContext
from integrations.slack.slack_types import SlackViewInterface, StartingConvoException
from integrations.utils import CONVERSATION_URL, get_final_agent_observation
from integrations.slack.slack_v1_callback_processor import SlackV1CallbackProcessor
from integrations.utils import (
CONVERSATION_URL,
ENABLE_V1_SLACK_RESOLVER,
get_final_agent_observation,
get_user_v1_enabled_setting,
)
from jinja2 import Environment
from slack_sdk import WebClient
from storage.slack_conversation import SlackConversation
@@ -10,21 +18,34 @@ from storage.slack_conversation_store import SlackConversationStore
from storage.slack_team_store import SlackTeamStore
from storage.slack_user import SlackUser
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartRequest,
AppConversationStartTaskStatus,
SendMessageRequest,
)
from openhands.app_server.config import get_app_conversation_service
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 USER_CONTEXT_ATTR
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.integrations.provider import ProviderHandler, ProviderType
from openhands.sdk import TextContent
from openhands.server.services.conversation_service import (
create_new_conversation,
setup_init_conversation_settings,
)
from openhands.server.shared import ConversationStoreImpl, config, conversation_manager
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,
)
from openhands.utils.async_utils import GENERAL_TIMEOUT, call_async_from_sync
# =================================================
# SECTION: Github view types
# SECTION: Slack view types
# =================================================
@@ -33,6 +54,10 @@ slack_conversation_store = SlackConversationStore.get_instance()
slack_team_store = SlackTeamStore.get_instance()
async def is_v1_enabled_for_slack_resolver(user_id: str) -> bool:
return await get_user_v1_enabled_setting(user_id) and ENABLE_V1_SLACK_RESOLVER
@dataclass
class SlackUnkownUserView(SlackViewInterface):
bot_access_token: str
@@ -48,6 +73,7 @@ class SlackUnkownUserView(SlackViewInterface):
send_summary_instruction: bool
conversation_id: str
team_id: str
v1_enabled: bool
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
raise NotImplementedError
@@ -55,9 +81,6 @@ class SlackUnkownUserView(SlackViewInterface):
async def create_or_update_conversation(self, jinja_env: Environment):
raise NotImplementedError
def get_callback_id(self) -> str:
raise NotImplementedError
def get_response_msg(self) -> str:
raise NotImplementedError
@@ -77,6 +100,7 @@ class SlackNewConversationView(SlackViewInterface):
send_summary_instruction: bool
conversation_id: str
team_id: str
v1_enabled: bool
def _get_initial_prompt(self, text: str, blocks: list[dict]):
bot_id = self._get_bot_id(blocks)
@@ -95,8 +119,7 @@ class SlackNewConversationView(SlackViewInterface):
return ''
def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
"Instructions passed when conversation is first initialized"
"""Instructions passed when conversation is first initialized"""
user_info: SlackUser = self.slack_to_openhands_user
messages = []
@@ -156,7 +179,7 @@ class SlackNewConversationView(SlackViewInterface):
'Attempting to start conversation without confirming selected repo from user'
)
async def save_slack_convo(self):
async def save_slack_convo(self, v1_enabled: bool = False):
if self.slack_to_openhands_user:
user_info: SlackUser = self.slack_to_openhands_user
@@ -166,49 +189,144 @@ class SlackNewConversationView(SlackViewInterface):
'channel_id': self.channel_id,
'conversation_id': self.conversation_id,
'keycloak_user_id': user_info.keycloak_user_id,
'org_id': user_info.org_id,
'parent_id': self.thread_ts or self.message_ts,
'v1_enabled': v1_enabled,
},
)
slack_conversation = SlackConversation(
conversation_id=self.conversation_id,
channel_id=self.channel_id,
keycloak_user_id=user_info.keycloak_user_id,
org_id=user_info.org_id,
parent_id=self.thread_ts
or self.message_ts, # conversations can start in a thread reply as well; we should always references the parent's (root level msg's) message ID
v1_enabled=v1_enabled,
)
await slack_conversation_store.create_slack_conversation(slack_conversation)
def _create_slack_v1_callback_processor(self) -> SlackV1CallbackProcessor:
"""Create a SlackV1CallbackProcessor for V1 conversation handling."""
return SlackV1CallbackProcessor(
slack_view_data={
'channel_id': self.channel_id,
'message_ts': self.message_ts,
'thread_ts': self.thread_ts,
'team_id': self.team_id,
'slack_user_id': self.slack_user_id,
}
)
async def create_or_update_conversation(self, jinja: Environment) -> str:
"""
Only creates a new conversation
"""
"""Only creates a new conversation"""
self._verify_necessary_values_are_set()
provider_tokens = await self.saas_user_auth.get_provider_tokens()
user_secrets = await self.saas_user_auth.get_user_secrets()
user_secrets = await self.saas_user_auth.get_secrets()
# Check if V1 conversations are enabled for this user
self.v1_enabled = await is_v1_enabled_for_slack_resolver(
self.slack_to_openhands_user.keycloak_user_id
)
if self.v1_enabled:
# Use V1 app conversation service
await self._create_v1_conversation(jinja)
return self.conversation_id
else:
# Use existing V0 conversation service
await self._create_v0_conversation(jinja, provider_tokens, user_secrets)
return self.conversation_id
async def _create_v0_conversation(
self, jinja: Environment, provider_tokens, user_secrets
) -> None:
"""Create conversation using the legacy V0 system."""
user_instructions, conversation_instructions = self._get_instructions(jinja)
# Determine git provider from repository
git_provider = None
if self.selected_repo and provider_tokens:
provider_handler = ProviderHandler(provider_tokens)
repository = await provider_handler.verify_repo_provider(self.selected_repo)
git_provider = repository.git_provider
agent_loop_info = await create_new_conversation(
user_id=self.slack_to_openhands_user.keycloak_user_id,
git_provider_tokens=provider_tokens,
selected_repository=self.selected_repo,
selected_branch=None,
initial_user_msg=user_instructions,
conversation_instructions=conversation_instructions
if conversation_instructions
else None,
conversation_instructions=(
conversation_instructions if conversation_instructions else None
),
image_urls=None,
replay_json=None,
conversation_trigger=ConversationTrigger.SLACK,
custom_secrets=user_secrets.custom_secrets if user_secrets else None,
git_provider=git_provider,
)
self.conversation_id = agent_loop_info.conversation_id
await self.save_slack_convo()
return self.conversation_id
logger.info(f'[Slack]: Created V0 conversation: {self.conversation_id}')
await self.save_slack_convo(v1_enabled=False)
def get_callback_id(self) -> str:
return f'slack_{self.channel_id}_{self.message_ts}'
async def _create_v1_conversation(self, jinja: Environment) -> None:
"""Create conversation using the new V1 app conversation system."""
user_instructions, conversation_instructions = self._get_instructions(jinja)
# Create the initial message request
initial_message = SendMessageRequest(
role='user', content=[TextContent(text=user_instructions)]
)
# Create the Slack V1 callback processor
slack_callback_processor = self._create_slack_v1_callback_processor()
# Determine git provider from repository
git_provider = None
provider_tokens = await self.saas_user_auth.get_provider_tokens()
if self.selected_repo and provider_tokens:
provider_handler = ProviderHandler(provider_tokens)
repository = await provider_handler.verify_repo_provider(self.selected_repo)
git_provider = ProviderType(repository.git_provider.value)
# Get the app conversation service and start the conversation
injector_state = InjectorState()
# Create the V1 conversation start request with the callback processor
self.conversation_id = uuid4().hex
start_request = AppConversationStartRequest(
conversation_id=UUID(self.conversation_id),
system_message_suffix=conversation_instructions,
initial_message=initial_message,
selected_repository=self.selected_repo,
git_provider=git_provider,
title=f'Slack conversation from {self.slack_to_openhands_user.slack_display_name}',
trigger=ConversationTrigger.SLACK,
processors=[
slack_callback_processor
], # Pass the callback processor directly
)
# Set up the Slack user context for the V1 system
slack_user_context = ResolverUserContext(saas_user_auth=self.saas_user_auth)
setattr(injector_state, USER_CONTEXT_ATTR, slack_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'[Slack V1]: Created new conversation: {self.conversation_id}')
await self.save_slack_convo(v1_enabled=True)
def get_response_msg(self) -> str:
user_info: SlackUser = self.slack_to_openhands_user
@@ -245,30 +363,20 @@ class SlackUpdateExistingConversationView(SlackNewConversationView):
return user_message, ''
async def create_or_update_conversation(self, jinja: Environment) -> str:
"""
Send new user message to converation
"""
async def send_message_to_v0_conversation(self, jinja: Environment):
user_info: SlackUser = self.slack_to_openhands_user
saas_user_auth: UserAuth = self.saas_user_auth
user_id = user_info.keycloak_user_id
# Org management in the future will get rid of this
# For now, only user that created the conversation can send follow up messages to it
if user_id != self.slack_conversation.keycloak_user_id:
raise StartingConvoException(
f'{user_info.slack_display_name} is not authorized to send messages to this conversation.'
)
# Check if conversation has been deleted
# Update logic when soft delete is implemented
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
metadata = await conversation_store.get_metadata(self.conversation_id)
if not metadata:
raise StartingConvoException('Conversation no longer exists.')
saas_user_auth: UserAuth = self.saas_user_auth
provider_tokens = await saas_user_auth.get_provider_tokens()
try:
conversation_store = await ConversationStoreImpl.get_instance(
config, user_id
)
await conversation_store.get_metadata(self.conversation_id)
except FileNotFoundError:
raise StartingConvoException('Conversation no longer exists.')
# Should we raise here if there are no provider tokens?
providers_set = list(provider_tokens.keys()) if provider_tokens else []
@@ -293,12 +401,123 @@ class SlackUpdateExistingConversationView(SlackNewConversationView):
if not agent_state or agent_state == AgentState.LOADING:
raise StartingConvoException('Conversation is still starting')
user_msg, _ = self._get_instructions(jinja)
user_msg_action = MessageAction(content=user_msg)
instructions, _ = self._get_instructions(jinja)
user_msg = MessageAction(content=instructions)
await conversation_manager.send_event_to_conversation(
self.conversation_id, event_to_dict(user_msg_action)
self.conversation_id, event_to_dict(user_msg)
)
async def send_message_to_v1_conversation(self, jinja: Environment):
"""Send a message to a v1 conversation using the agent server API."""
# Import services within the method to avoid circular imports
from openhands.agent_server.models import SendMessageRequest
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.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(
UUID(self.conversation_id)
),
UUID(self.conversation_id),
)
# 2. Sandbox lookup + validation
sandbox = await sandbox_service.get_sandbox(
app_conversation_info.sandbox_id
)
if sandbox and sandbox.status == SandboxStatus.PAUSED:
# Resume paused sandbox and wait for it to be running
logger.info('[Slack V1]: Attempting to resume paused sandbox')
await sandbox_service.resume_sandbox(app_conversation_info.sandbox_id)
# Wait for sandbox to be running (handles both fresh start and resume)
running_sandbox = await sandbox_service.wait_for_sandbox_running(
app_conversation_info.sandbox_id,
timeout=120,
poll_interval=2,
httpx_client=httpx_client,
)
assert (
running_sandbox.session_api_key is not None
), f'No session API key for sandbox: {running_sandbox.id}'
# 3. Get the agent server URL
agent_server_url = get_agent_server_url_from_sandbox(running_sandbox)
# 4. Prepare the message content
user_msg, _ = self._get_instructions(jinja)
# 5. Create the message request
send_message_request = SendMessageRequest(
role='user', content=[TextContent(text=user_msg)], run=True
)
# 6. Send the message to the agent server
url = f'{agent_server_url.rstrip("/")}/api/conversations/{UUID(self.conversation_id)}/events'
headers = {'X-Session-API-Key': running_sandbox.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()
except Exception as e:
logger.error(
'[Slack V1] Failed to send message to conversation %s: %s',
self.conversation_id,
str(e),
exc_info=True,
)
raise Exception(f'Failed to send message to v1 conversation: {str(e)}')
async def create_or_update_conversation(self, jinja: Environment) -> str:
"""Send new user message to converation"""
user_info: SlackUser = self.slack_to_openhands_user
user_id = user_info.keycloak_user_id
# Org management in the future will get rid of this
# For now, only user that created the conversation can send follow up messages to it
if user_id != self.slack_conversation.keycloak_user_id:
raise StartingConvoException(
f'{user_info.slack_display_name} is not authorized to send messages to this conversation.'
)
if self.slack_conversation.v1_enabled:
await self.send_message_to_v1_conversation(jinja)
else:
await self.send_message_to_v0_conversation(jinja)
return self.conversation_id
def get_response_msg(self):
@@ -350,7 +569,7 @@ class SlackFactory:
'channel_id': channel_id,
},
)
raise Exception('Did not slack team')
raise Exception('Did not find slack team')
# Determine if this is a known slack user by openhands
if not slack_user or not saas_user_auth or not channel_id:
@@ -368,6 +587,7 @@ class SlackFactory:
send_summary_instruction=False,
conversation_id='',
team_id=team_id,
v1_enabled=False,
)
conversation: SlackConversation | None = call_async_from_sync(
@@ -398,6 +618,7 @@ class SlackFactory:
conversation_id=conversation.conversation_id,
slack_conversation=conversation,
team_id=team_id,
v1_enabled=False,
)
elif SlackFactory.did_user_select_repo_from_form(message):
@@ -415,6 +636,7 @@ class SlackFactory:
send_summary_instruction=True,
conversation_id='',
team_id=team_id,
v1_enabled=False,
)
else:
@@ -432,4 +654,5 @@ class SlackFactory:
send_summary_instruction=True,
conversation_id='',
team_id=team_id,
v1_enabled=False,
)

View File

@@ -0,0 +1,53 @@
from storage.repository_store import RepositoryStore
from storage.stored_repository import StoredRepository
from storage.user_repo_map import UserRepositoryMap
from storage.user_repo_map_store import UserRepositoryMapStore
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import Repository
async def store_repositories_in_db(repos: list[Repository], user_id: str) -> None:
"""
Store repositories in DB and create user-repository mappings
Args:
repos: List of Repository objects to store
user_id: User ID associated with these repositories
"""
# Convert Repository objects to StoredRepository objects
# Convert Repository objects to UserRepositoryMap objects
stored_repos = []
user_repos = []
for repo in repos:
repo_id = f'{repo.git_provider.value}##{str(repo.id)}'
stored_repo = StoredRepository(
repo_name=repo.full_name,
repo_id=repo_id,
is_public=repo.is_public,
# Optional fields set to None by default
has_microagent=None,
has_setup_script=None,
)
stored_repos.append(stored_repo)
user_repo_map = UserRepositoryMap(user_id=user_id, repo_id=repo_id, admin=None)
user_repos.append(user_repo_map)
# Get config instance
config = OpenHandsConfig()
try:
# Store repositories in the repos table
repo_store = RepositoryStore.get_instance(config)
repo_store.store_projects(stored_repos)
# Store user-repository mappings in the user-repos table
user_repo_store = UserRepositoryMapStore.get_instance(config)
user_repo_store.store_user_repo_mappings(user_repos)
logger.info(f'Saved repos for user {user_id}')
except Exception:
logger.warning('Failed to save repos', exc_info=True)

View File

@@ -1,19 +1,24 @@
from uuid import UUID
import stripe
from server.auth.token_manager import TokenManager
from server.constants import STRIPE_API_KEY
from server.logger import logger
from sqlalchemy.orm import Session
from storage.database import session_maker
from storage.org import Org
from storage.org_store import OrgStore
from storage.stripe_customer import StripeCustomer
from openhands.utils.async_utils import call_sync_from_async
stripe.api_key = STRIPE_API_KEY
async def find_customer_id_by_user_id(user_id: str) -> str | None:
# First search our own DB...
async def find_customer_id_by_org_id(org_id: UUID) -> str | None:
with session_maker() as session:
stripe_customer = (
session.query(StripeCustomer)
.filter(StripeCustomer.keycloak_user_id == user_id)
.filter(StripeCustomer.org_id == org_id)
.first()
)
if stripe_customer:
@@ -21,46 +26,76 @@ async def find_customer_id_by_user_id(user_id: str) -> str | None:
# If that fails, fallback to stripe
search_result = await stripe.Customer.search_async(
query=f"metadata['user_id']:'{user_id}'",
query=f"metadata['org_id']:'{str(org_id)}'",
)
data = search_result.data
if not data:
logger.info('no_customer_for_user_id', extra={'user_id': user_id})
logger.info(
'no_customer_for_org_id',
extra={'org_id': str(org_id)},
)
return None
return data[0].id # type: ignore [attr-defined]
async def find_or_create_customer(user_id: str) -> str:
customer_id = await find_customer_id_by_user_id(user_id)
if customer_id:
return customer_id
logger.info('creating_customer', extra={'user_id': user_id})
async def find_customer_id_by_user_id(user_id: str) -> str | None:
# First search our own DB...
org = await call_sync_from_async(
OrgStore.get_current_org_from_keycloak_user_id, user_id
)
if not org:
logger.warning(f'Org not found for user {user_id}')
return None
customer_id = await find_customer_id_by_org_id(org.id)
return customer_id
# Get the user info from keycloak
token_manager = TokenManager()
user_info = await token_manager.get_user_info_from_user_id(user_id) or {}
async def find_or_create_customer_by_user_id(user_id: str) -> dict | None:
# Get the current org for the user
org = await call_sync_from_async(
OrgStore.get_current_org_from_keycloak_user_id, user_id
)
if not org:
logger.warning(f'Org not found for user {user_id}')
return None
customer_id = await find_customer_id_by_org_id(org.id)
if customer_id:
return {'customer_id': customer_id, 'org_id': str(org.id)}
logger.info(
'creating_customer',
extra={'user_id': user_id, 'org_id': str(org.id)},
)
# Create the customer in stripe
customer = await stripe.Customer.create_async(
email=str(user_info.get('email', '')),
metadata={'user_id': user_id},
email=org.contact_email,
metadata={'org_id': str(org.id)},
)
# Save the stripe customer in the local db
with session_maker() as session:
session.add(
StripeCustomer(keycloak_user_id=user_id, stripe_customer_id=customer.id)
StripeCustomer(
keycloak_user_id=user_id,
org_id=org.id,
stripe_customer_id=customer.id,
)
)
session.commit()
logger.info(
'created_customer',
extra={'user_id': user_id, 'stripe_customer_id': customer.id},
extra={
'user_id': user_id,
'org_id': str(org.id),
'stripe_customer_id': customer.id,
},
)
return customer.id
return {'customer_id': customer.id, 'org_id': str(org.id)}
async def has_payment_method(user_id: str) -> bool:
async def has_payment_method_by_user_id(user_id: str) -> bool:
customer_id = await find_customer_id_by_user_id(user_id)
if customer_id is None:
return False
@@ -71,3 +106,28 @@ async def has_payment_method(user_id: str) -> bool:
f'has_payment_method:{user_id}:{customer_id}:{bool(payment_methods.data)}'
)
return bool(payment_methods.data)
async def migrate_customer(session: Session, user_id: str, org: Org):
stripe_customer = (
session.query(StripeCustomer)
.filter(StripeCustomer.keycloak_user_id == user_id)
.first()
)
if stripe_customer is None:
return
stripe_customer.org_id = org.id
customer = await stripe.Customer.modify_async(
id=stripe_customer.stripe_customer_id,
email=org.contact_email,
metadata={'user_id': '', 'org_id': str(org.id)},
)
logger.info(
'migrated_customer',
extra={
'user_id': user_id,
'org_id': str(org.id),
'stripe_customer_id': customer.id,
},
)

View File

@@ -19,7 +19,7 @@ class PRStatus(Enum):
class UserData(BaseModel):
user_id: int
username: str
keycloak_user_id: str | None
keycloak_user_id: str
@dataclass
@@ -45,7 +45,3 @@ class ResolverViewInterface(SummaryExtractionTracker):
async def create_new_conversation(self, jinja_env: Environment, token: str):
"Create a new conversation"
raise NotImplementedError()
def get_callback_id(self) -> str:
"Unique callback id for subscribription made to EventStream for fetching agent summary"
raise NotImplementedError()

View File

@@ -7,12 +7,8 @@ from typing import TYPE_CHECKING
from jinja2 import Environment, FileSystemLoader
from server.constants import WEB_HOST
from storage.repository_store import RepositoryStore
from storage.stored_repository import StoredRepository
from storage.user_repo_map import UserRepositoryMap
from storage.user_repo_map_store import UserRepositoryMapStore
from storage.org_store import OrgStore
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.schema.agent import AgentState
from openhands.events import Event, EventSource
@@ -20,10 +16,12 @@ from openhands.events.action import (
AgentFinishAction,
MessageAction,
)
from openhands.events.event_filter import EventFilter
from openhands.events.event_store_abc import EventStoreABC
from openhands.events.observation.agent import AgentStateChangedObservation
from openhands.integrations.service_types import Repository
from openhands.storage.data_models.conversation_status import ConversationStatus
from openhands.utils.async_utils import call_sync_from_async
if TYPE_CHECKING:
from openhands.server.conversation_manager.conversation_manager import (
@@ -35,7 +33,7 @@ if TYPE_CHECKING:
HOST = WEB_HOST
# ---- DO NOT REMOVE ----
HOST_URL = f'https://{HOST}'
HOST_URL = f'https://{HOST}' if 'localhost' not in HOST else f'http://{HOST}'
GITHUB_WEBHOOK_URL = f'{HOST_URL}/integration/github/events'
GITLAB_WEBHOOK_URL = f'{HOST_URL}/integration/gitlab/events'
conversation_prefix = 'conversations/{}'
@@ -46,13 +44,45 @@ ENABLE_PROACTIVE_CONVERSATION_STARTERS = (
os.getenv('ENABLE_PROACTIVE_CONVERSATION_STARTERS', 'false').lower() == 'true'
)
def get_session_expired_message(username: str | None = None) -> str:
"""Get a user-friendly session expired message.
Used by integrations to notify users when their Keycloak offline session
has expired.
Args:
username: Optional username to mention in the message. If provided,
the message will include @username prefix (used by Git providers
like GitHub, GitLab, Slack). If None, returns a generic message
(used by Jira, Jira DC, Linear).
Returns:
A formatted session expired message
"""
if username:
return f'@{username} your session has expired. Please login again at [OpenHands Cloud]({HOST_URL}) and try again.'
return f'Your session has expired. Please login again at [OpenHands Cloud]({HOST_URL}) and try again.'
# Toggle for solvability report feature
ENABLE_SOLVABILITY_ANALYSIS = (
os.getenv('ENABLE_SOLVABILITY_ANALYSIS', 'false').lower() == 'true'
)
# Toggle for V1 GitHub resolver feature
ENABLE_V1_GITHUB_RESOLVER = (
os.getenv('ENABLE_V1_GITHUB_RESOLVER', 'false').lower() == 'true'
)
OPENHANDS_RESOLVER_TEMPLATES_DIR = 'openhands/integrations/templates/resolver/'
ENABLE_V1_SLACK_RESOLVER = (
os.getenv('ENABLE_V1_SLACK_RESOLVER', 'false').lower() == 'true'
)
OPENHANDS_RESOLVER_TEMPLATES_DIR = (
os.getenv('OPENHANDS_RESOLVER_TEMPLATES_DIR')
or 'openhands/integrations/templates/resolver/'
)
jinja_env = Environment(loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR))
@@ -80,6 +110,28 @@ def get_summary_instruction():
return summary_instruction
async def get_user_v1_enabled_setting(user_id: str | None) -> bool:
"""Get the user's V1 conversation API setting.
Args:
user_id: The keycloak user ID
Returns:
True if V1 conversations are enabled for this user, False otherwise
"""
if not user_id:
return False
org = await call_sync_from_async(
OrgStore.get_current_org_from_keycloak_user_id, user_id
)
if not org or org.v1_enabled is None:
return False
return org.v1_enabled
def has_exact_mention(text: str, mention: str) -> bool:
"""Check if the text contains an exact mention (not part of a larger word).
@@ -198,18 +250,35 @@ def get_summary_for_agent_state(
def get_final_agent_observation(
event_store: EventStoreABC,
) -> list[AgentStateChangedObservation]:
return event_store.get_matching_events(
source=EventSource.ENVIRONMENT,
event_types=(AgentStateChangedObservation,),
limit=1,
reverse=True,
events = list(
event_store.search_events(
filter=EventFilter(
source=EventSource.ENVIRONMENT,
include_types=(AgentStateChangedObservation,),
),
limit=1,
reverse=True,
)
)
result = [e for e in events if isinstance(e, AgentStateChangedObservation)]
assert len(result) == len(events)
return result
def get_last_user_msg(event_store: EventStoreABC) -> list[MessageAction]:
return event_store.get_matching_events(
source=EventSource.USER, event_types=(MessageAction,), limit=1, reverse='true'
events = list(
event_store.search_events(
filter=EventFilter(
source=EventSource.USER,
include_types=(MessageAction,),
),
limit=1,
reverse=True,
)
)
result = [e for e in events if isinstance(e, MessageAction)]
assert len(result) == len(events)
return result
def extract_summary_from_event_store(
@@ -221,18 +290,22 @@ def extract_summary_from_event_store(
conversation_link = CONVERSATION_URL.format(conversation_id)
summary_instruction = get_summary_instruction()
instruction_event: list[MessageAction] = event_store.get_matching_events(
query=json.dumps(summary_instruction),
source=EventSource.USER,
event_types=(MessageAction,),
limit=1,
reverse=True,
instruction_events = list(
event_store.search_events(
filter=EventFilter(
query=json.dumps(summary_instruction),
source=EventSource.USER,
include_types=(MessageAction,),
),
limit=1,
reverse=True,
)
)
final_agent_observation = get_final_agent_observation(event_store)
# Find summary instruction event ID
if len(instruction_event) == 0:
if not instruction_events:
logger.warning(
'no_instruction_event_found', extra={'conversation_id': conversation_id}
)
@@ -240,19 +313,19 @@ def extract_summary_from_event_store(
final_agent_observation, conversation_link
) # Agent did not receive summary instruction
event_id: int = instruction_event[0].id
agent_messages: list[MessageAction | AgentFinishAction] = (
event_store.get_matching_events(
start_id=event_id,
source=EventSource.AGENT,
event_types=(MessageAction, AgentFinishAction),
reverse=True,
summary_events = list(
event_store.search_events(
filter=EventFilter(
source=EventSource.AGENT,
include_types=(MessageAction, AgentFinishAction),
),
limit=1,
reverse=True,
start_id=instruction_events[0].id,
)
)
if len(agent_messages) == 0:
if not summary_events:
logger.warning(
'no_agent_messages_found', extra={'conversation_id': conversation_id}
)
@@ -260,10 +333,11 @@ def extract_summary_from_event_store(
final_agent_observation, conversation_link
) # Agent failed to generate summary
summary_event: MessageAction | AgentFinishAction = agent_messages[0]
summary_event = summary_events[0]
if isinstance(summary_event, MessageAction):
return summary_event.content
assert isinstance(summary_event, AgentFinishAction)
return summary_event.final_thought
@@ -316,106 +390,50 @@ def append_conversation_footer(message: str, conversation_id: str) -> str:
The message with the conversation footer appended
"""
conversation_link = CONVERSATION_URL.format(conversation_id)
footer = f'\n\n<sub>[View full conversation]({conversation_link})</sub>'
footer = f'\n\n[View full conversation]({conversation_link})'
return message + footer
async def store_repositories_in_db(repos: list[Repository], user_id: str) -> None:
"""
Store repositories in DB and create user-repository mappings
Args:
repos: List of Repository objects to store
user_id: User ID associated with these repositories
"""
# Convert Repository objects to StoredRepository objects
# Convert Repository objects to UserRepositoryMap objects
stored_repos = []
user_repos = []
for repo in repos:
repo_id = f'{repo.git_provider.value}##{str(repo.id)}'
stored_repo = StoredRepository(
repo_name=repo.full_name,
repo_id=repo_id,
is_public=repo.is_public,
# Optional fields set to None by default
has_microagent=None,
has_setup_script=None,
)
stored_repos.append(stored_repo)
user_repo_map = UserRepositoryMap(user_id=user_id, repo_id=repo_id, admin=None)
user_repos.append(user_repo_map)
# Get config instance
config = OpenHandsConfig()
try:
# Store repositories in the repos table
repo_store = RepositoryStore.get_instance(config)
repo_store.store_projects(stored_repos)
# Store user-repository mappings in the user-repos table
user_repo_store = UserRepositoryMapStore.get_instance(config)
user_repo_store.store_user_repo_mappings(user_repos)
logger.info(f'Saved repos for user {user_id}')
except Exception:
logger.warning('Failed to save repos', exc_info=True)
def infer_repo_from_message(user_msg: str) -> list[str]:
"""
Extract all repository names in the format 'owner/repo' from various Git provider URLs
and direct mentions in text. Supports GitHub, GitLab, and BitBucket.
Args:
user_msg: Input message that may contain repository references
Returns:
List of repository names in 'owner/repo' format, empty list if none found
"""
# Normalize the message by removing extra whitespace and newlines
normalized_msg = re.sub(r'\s+', ' ', user_msg.strip())
# Pattern to match Git URLs from GitHub, GitLab, and BitBucket
# Captures: protocol, domain, owner, repo (with optional .git extension)
git_url_pattern = r'https?://(?:github\.com|gitlab\.com|bitbucket\.org)/([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+?)(?:\.git)?(?:[/?#].*?)?(?=\s|$|[^\w.-])'
# Pattern to match direct owner/repo mentions (e.g., "All-Hands-AI/OpenHands")
# Must be surrounded by word boundaries or specific characters to avoid false positives
direct_pattern = (
r'(?:^|\s|[\[\(\'"])([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)(?=\s|$|[\]\)\'",.])'
git_url_pattern = (
r'https?://(?:github\.com|gitlab\.com|bitbucket\.org)/'
r'([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+?)(?:\.git)?'
r'(?:[/?#].*?)?(?=\s|$|[^\w.-])'
)
matches = []
# UPDATED: allow {{ owner/repo }} in addition to existing boundaries
direct_pattern = (
r'(?:^|\s|{{|[\[\(\'":`])' # left boundary
r'([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)'
r'(?=\s|$|}}|[\]\)\'",.:`])' # right boundary
)
# First, find all Git URLs (highest priority)
git_matches = re.findall(git_url_pattern, normalized_msg)
for owner, repo in git_matches:
# Remove .git extension if present
matches: list[str] = []
# Git URLs first (highest priority)
for owner, repo in re.findall(git_url_pattern, normalized_msg):
repo = re.sub(r'\.git$', '', repo)
matches.append(f'{owner}/{repo}')
# Second, find all direct owner/repo mentions
direct_matches = re.findall(direct_pattern, normalized_msg)
for owner, repo in direct_matches:
# Direct mentions
for owner, repo in re.findall(direct_pattern, normalized_msg):
full_match = f'{owner}/{repo}'
# Skip if it looks like a version number, date, or file path
if (
re.match(r'^\d+\.\d+/\d+\.\d+$', full_match) # version numbers
or re.match(r'^\d{1,2}/\d{1,2}$', full_match) # dates
or re.match(r'^[A-Z]/[A-Z]$', full_match) # single letters
or repo.endswith('.txt')
or repo.endswith('.md') # file extensions
or repo.endswith('.py')
or repo.endswith('.js')
or '.' in repo
and len(repo.split('.')) > 2
): # complex file paths
re.match(r'^\d+\.\d+/\d+\.\d+$', full_match)
or re.match(r'^\d{1,2}/\d{1,2}$', full_match)
or re.match(r'^[A-Z]/[A-Z]$', full_match)
or repo.endswith(('.txt', '.md', '.py', '.js'))
or ('.' in repo and len(repo.split('.')) > 2)
):
continue
# Avoid duplicates from Git URLs already found
if full_match not in matches:
matches.append(full_match)

View File

@@ -0,0 +1,20 @@
from pydantic import SecretStr
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from openhands.core.logger import openhands_logger as logger
from openhands.server.user_auth.user_auth import UserAuth
async def get_saas_user_auth(
keycloak_user_id: str, token_manager: TokenManager
) -> UserAuth:
offline_token = await token_manager.load_offline_token(keycloak_user_id)
if offline_token is None:
logger.info('no_offline_token_found')
user_auth = SaasUserAuth(
user_id=keycloak_user_id,
refresh_token=SecretStr(offline_token),
)
return user_auth

View File

@@ -20,6 +20,8 @@ down_revision = '059'
branch_labels = None
depends_on = None
# TODO: decide whether to modify this for orgs or users
def upgrade():
"""
@@ -28,8 +30,10 @@ def upgrade():
This replaces the functionality of the removed admin maintenance endpoint.
"""
# Import here to avoid circular imports
from server.constants import CURRENT_USER_SETTINGS_VERSION
# Hardcoded value to prevent migration failures when constant is removed from codebase
# This migration has already run in production, so we use the value that was current at the time
CURRENT_USER_SETTINGS_VERSION = 4
# Create a connection and bind it to a session
connection = op.get_bind()

View File

@@ -0,0 +1,259 @@
"""Sync DB with Models
Revision ID: 076
Revises: 075
Create Date: 2025-10-05 11:28:41.772294
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartTaskStatus,
)
from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResultStatus,
)
# revision identifiers, used by Alembic.
revision: str = '076'
down_revision: Union[str, Sequence[str], None] = '075'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
'conversation_metadata',
sa.Column('max_budget_per_task', sa.Float(), nullable=True),
)
op.add_column(
'conversation_metadata',
sa.Column('cache_read_tokens', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('cache_write_tokens', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('reasoning_tokens', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('context_window', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('per_turn_token', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column(
'conversation_version', sa.String(), nullable=False, server_default='V0'
),
)
op.create_index(
op.f('ix_conversation_metadata_conversation_version'),
'conversation_metadata',
['conversation_version'],
unique=False,
)
op.add_column('conversation_metadata', sa.Column('sandbox_id', sa.String()))
op.create_index(
op.f('ix_conversation_metadata_sandbox_id'),
'conversation_metadata',
['sandbox_id'],
unique=False,
)
op.create_table(
'app_conversation_start_task',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('created_by_user_id', sa.String(), nullable=True),
sa.Column('status', sa.Enum(AppConversationStartTaskStatus), nullable=True),
sa.Column('detail', sa.String(), nullable=True),
sa.Column('app_conversation_id', sa.UUID(), nullable=True),
sa.Column('sandbox_id', sa.String(), nullable=True),
sa.Column('agent_server_url', sa.String(), nullable=True),
sa.Column('request', sa.JSON(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_app_conversation_start_task_created_at'),
'app_conversation_start_task',
['created_at'],
unique=False,
)
op.create_index(
op.f('ix_app_conversation_start_task_created_by_user_id'),
'app_conversation_start_task',
['created_by_user_id'],
unique=False,
)
op.create_index(
op.f('ix_app_conversation_start_task_updated_at'),
'app_conversation_start_task',
['updated_at'],
unique=False,
)
op.create_table(
'event_callback',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('conversation_id', sa.UUID(), nullable=True),
sa.Column('processor', sa.JSON(), nullable=True),
sa.Column('event_kind', sa.String(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_event_callback_created_at'),
'event_callback',
['created_at'],
unique=False,
)
op.create_table(
'event_callback_result',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('status', sa.Enum(EventCallbackResultStatus), nullable=True),
sa.Column('event_callback_id', sa.UUID(), nullable=True),
sa.Column('event_id', sa.UUID(), nullable=True),
sa.Column('conversation_id', sa.UUID(), nullable=True),
sa.Column('detail', sa.String(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_event_callback_result_conversation_id'),
'event_callback_result',
['conversation_id'],
unique=False,
)
op.create_index(
op.f('ix_event_callback_result_created_at'),
'event_callback_result',
['created_at'],
unique=False,
)
op.create_index(
op.f('ix_event_callback_result_event_callback_id'),
'event_callback_result',
['event_callback_id'],
unique=False,
)
op.create_index(
op.f('ix_event_callback_result_event_id'),
'event_callback_result',
['event_id'],
unique=False,
)
op.create_table(
'v1_remote_sandbox',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_by_user_id', sa.String(), nullable=True),
sa.Column('sandbox_spec_id', sa.String(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_v1_remote_sandbox_created_at'),
'v1_remote_sandbox',
['created_at'],
unique=False,
)
op.create_index(
op.f('ix_v1_remote_sandbox_created_by_user_id'),
'v1_remote_sandbox',
['created_by_user_id'],
unique=False,
)
op.create_index(
op.f('ix_v1_remote_sandbox_sandbox_spec_id'),
'v1_remote_sandbox',
['sandbox_spec_id'],
unique=False,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f('ix_v1_remote_sandbox_sandbox_spec_id'), table_name='v1_remote_sandbox'
)
op.drop_index(
op.f('ix_v1_remote_sandbox_created_by_user_id'), table_name='v1_remote_sandbox'
)
op.drop_index(
op.f('ix_v1_remote_sandbox_created_at'), table_name='v1_remote_sandbox'
)
op.drop_table('v1_remote_sandbox')
op.drop_index(
op.f('ix_event_callback_result_event_id'),
table_name='event_callback_result',
)
op.drop_index(
op.f('ix_event_callback_result_event_callback_id'),
table_name='event_callback_result',
)
op.drop_index(
op.f('ix_event_callback_result_created_at'),
table_name='event_callback_result',
)
op.drop_index(
op.f('ix_event_callback_result_conversation_id'),
table_name='event_callback_result',
)
op.drop_table('event_callback_result')
op.drop_index(op.f('ix_event_callback_created_at'), table_name='event_callback')
op.drop_table('event_callback')
op.drop_index(
op.f('ix_app_conversation_start_task_updated_at'),
table_name='app_conversation_start_task',
)
op.drop_index(
op.f('ix_app_conversation_start_task_created_by_user_id'),
table_name='app_conversation_start_task',
)
op.drop_index(
op.f('ix_app_conversation_start_task_created_at'),
table_name='app_conversation_start_task',
)
op.drop_table('app_conversation_start_task')
op.drop_column('conversation_metadata', 'sandbox_id')
op.drop_column('conversation_metadata', 'conversation_version')
op.drop_column('conversation_metadata', 'per_turn_token')
op.drop_column('conversation_metadata', 'context_window')
op.drop_column('conversation_metadata', 'reasoning_tokens')
op.drop_column('conversation_metadata', 'cache_write_tokens')
op.drop_column('conversation_metadata', 'cache_read_tokens')
op.drop_column('conversation_metadata', 'max_budget_per_task')
op.execute('DROP TYPE appconversationstarttaskstatus')
op.execute('DROP TYPE eventcallbackresultstatus')
# ### end Alembic commands ###

View File

@@ -0,0 +1,27 @@
"""drop settings table
Revision ID: 077
Revises: 076
Create Date: 2025-10-21 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '077'
down_revision: Union[str, None] = '076'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Drop the deprecated settings table."""
op.execute('DROP TABLE IF EXISTS settings')
def downgrade() -> None:
"""No-op downgrade since the settings table is deprecated."""
pass

View File

@@ -0,0 +1,129 @@
"""create telemetry tables
Revision ID: 078
Revises: 077
Create Date: 2025-10-21
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '078'
down_revision: Union[str, None] = '077'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create telemetry tables for metrics collection and configuration."""
# Create telemetry_metrics table
op.create_table(
'telemetry_metrics',
sa.Column(
'id',
sa.String(), # UUID as string
nullable=False,
primary_key=True,
),
sa.Column(
'collected_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
),
sa.Column(
'metrics_data',
sa.JSON(),
nullable=False,
),
sa.Column(
'uploaded_at',
sa.DateTime(timezone=True),
nullable=True,
),
sa.Column(
'upload_attempts',
sa.Integer(),
nullable=False,
server_default='0',
),
sa.Column(
'last_upload_error',
sa.Text(),
nullable=True,
),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
),
sa.Column(
'updated_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
),
)
# Create indexes for telemetry_metrics
op.create_index(
'ix_telemetry_metrics_collected_at', 'telemetry_metrics', ['collected_at']
)
op.create_index(
'ix_telemetry_metrics_uploaded_at', 'telemetry_metrics', ['uploaded_at']
)
# Create telemetry_replicated_identity table (minimal persistent identity data)
op.create_table(
'telemetry_replicated_identity',
sa.Column(
'id',
sa.Integer(),
nullable=False,
primary_key=True,
server_default='1',
),
sa.Column(
'customer_id',
sa.String(255),
nullable=True,
),
sa.Column(
'instance_id',
sa.String(255),
nullable=True,
),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
),
sa.Column(
'updated_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
),
)
# Add constraint to ensure single row in telemetry_replicated_identity
op.create_check_constraint(
'single_identity_row', 'telemetry_replicated_identity', 'id = 1'
)
def downgrade() -> None:
"""Drop telemetry tables."""
# Drop indexes first
op.drop_index('ix_telemetry_metrics_uploaded_at', 'telemetry_metrics')
op.drop_index('ix_telemetry_metrics_collected_at', 'telemetry_metrics')
# Drop tables
op.drop_table('telemetry_replicated_identity')
op.drop_table('telemetry_metrics')

View File

@@ -0,0 +1,39 @@
"""rename user_secrets table to custom_secrets
Revision ID: 079
Revises: 078
Create Date: 2025-10-27 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '079'
down_revision: Union[str, None] = '078'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Rename the table from user_secrets to custom_secrets
op.rename_table('user_secrets', 'custom_secrets')
# Rename the index to match the new table name
op.drop_index('idx_user_secrets_keycloak_user_id', 'custom_secrets')
op.create_index(
'idx_custom_secrets_keycloak_user_id', 'custom_secrets', ['keycloak_user_id']
)
def downgrade() -> None:
# Rename the index back to the original name
op.drop_index('idx_custom_secrets_keycloak_user_id', 'custom_secrets')
op.create_index(
'idx_user_secrets_keycloak_user_id', 'custom_secrets', ['keycloak_user_id']
)
# Rename the table back from custom_secrets to user_secrets
op.rename_table('custom_secrets', 'user_secrets')

View File

@@ -0,0 +1,71 @@
"""add status and updated_at to callback
Revision ID: 080
Revises: 079
Create Date: 2025-11-05 00:00:00.000000
"""
from enum import Enum
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '080'
down_revision: Union[str, None] = '079'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
class EventCallbackStatus(Enum):
ACTIVE = 'ACTIVE'
DISABLED = 'DISABLED'
COMPLETED = 'COMPLETED'
ERROR = 'ERROR'
def upgrade() -> None:
"""Upgrade schema."""
status = sa.Enum(EventCallbackStatus, name='eventcallbackstatus')
status.create(op.get_bind(), checkfirst=True)
op.add_column(
'event_callback',
sa.Column('status', status, nullable=False, server_default='ACTIVE'),
)
op.add_column(
'event_callback',
sa.Column(
'updated_at', sa.DateTime, nullable=False, server_default=sa.func.now()
),
)
op.drop_index('ix_event_callback_result_event_id')
op.drop_column('event_callback_result', 'event_id')
op.add_column(
'event_callback_result', sa.Column('event_id', sa.String, nullable=True)
)
op.create_index(
op.f('ix_event_callback_result_event_id'),
'event_callback_result',
['event_id'],
unique=False,
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_column('event_callback', 'status')
op.drop_column('event_callback', 'updated_at')
op.drop_index('ix_event_callback_result_event_id')
op.drop_column('event_callback_result', 'event_id')
op.add_column(
'event_callback_result', sa.Column('event_id', sa.UUID, nullable=True)
)
op.create_index(
op.f('ix_event_callback_result_event_id'),
'event_callback_result',
['event_id'],
unique=False,
)
op.execute('DROP TYPE eventcallbackstatus')

View File

@@ -0,0 +1,41 @@
"""add parent_conversation_id to conversation_metadata
Revision ID: 081
Revises: 080
Create Date: 2025-11-06 00:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '081'
down_revision: Union[str, None] = '080'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
'conversation_metadata',
sa.Column('parent_conversation_id', sa.String(), nullable=True),
)
op.create_index(
op.f('ix_conversation_metadata_parent_conversation_id'),
'conversation_metadata',
['parent_conversation_id'],
unique=False,
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(
op.f('ix_conversation_metadata_parent_conversation_id'),
table_name='conversation_metadata',
)
op.drop_column('conversation_metadata', 'parent_conversation_id')

View File

@@ -0,0 +1,51 @@
"""Add SETTING_UP_SKILLS to appconversationstarttaskstatus enum
Revision ID: 082
Revises: 081
Create Date: 2025-11-19 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision: str = '082'
down_revision: Union[str, Sequence[str], None] = '081'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add SETTING_UP_SKILLS enum value to appconversationstarttaskstatus."""
# Check if the enum value already exists before adding it
# This handles the case where the enum was created with the value already included
connection = op.get_bind()
result = connection.execute(
text(
"SELECT 1 FROM pg_enum WHERE enumlabel = 'SETTING_UP_SKILLS' "
"AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'appconversationstarttaskstatus')"
)
)
if not result.fetchone():
# Add the new enum value only if it doesn't already exist
op.execute(
"ALTER TYPE appconversationstarttaskstatus ADD VALUE 'SETTING_UP_SKILLS'"
)
def downgrade() -> None:
"""Remove SETTING_UP_SKILLS enum value from appconversationstarttaskstatus.
Note: PostgreSQL doesn't support removing enum values directly.
This would require recreating the enum type and updating all references.
For safety, this downgrade is not implemented.
"""
# PostgreSQL doesn't support removing enum values directly
# This would require a complex migration to recreate the enum
# For now, we'll leave this as a no-op since removing enum values
# is rarely needed and can be dangerous
pass

View File

@@ -0,0 +1,35 @@
"""Add v1_enabled column to user_settings
Revision ID: 083
Revises: 082
Create Date: 2025-11-18 00:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '083'
down_revision: Union[str, None] = '082'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add v1_enabled column to user_settings table."""
op.add_column(
'user_settings',
sa.Column(
'v1_enabled',
sa.Boolean(),
nullable=True,
),
)
def downgrade() -> None:
"""Remove v1_enabled column from user_settings table."""
op.drop_column('user_settings', 'v1_enabled')

View File

@@ -0,0 +1,49 @@
"""Create device_codes table for OAuth 2.0 Device Flow
Revision ID: 084
Revises: 083
Create Date: 2024-12-10 12:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '084'
down_revision = '083'
branch_labels = None
depends_on = None
def upgrade():
"""Create device_codes table for OAuth 2.0 Device Flow."""
op.create_table(
'device_codes',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('device_code', sa.String(length=128), nullable=False),
sa.Column('user_code', sa.String(length=16), nullable=False),
sa.Column('status', sa.String(length=32), nullable=False),
sa.Column('keycloak_user_id', sa.String(length=255), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('authorized_at', sa.DateTime(timezone=True), nullable=True),
# Rate limiting fields for RFC 8628 section 3.5 compliance
sa.Column('last_poll_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('current_interval', sa.Integer(), nullable=False, default=5),
sa.PrimaryKeyConstraint('id'),
)
# Create indexes for efficient lookups
op.create_index(
'ix_device_codes_device_code', 'device_codes', ['device_code'], unique=True
)
op.create_index(
'ix_device_codes_user_code', 'device_codes', ['user_code'], unique=True
)
def downgrade():
"""Drop device_codes table."""
op.drop_index('ix_device_codes_user_code', table_name='device_codes')
op.drop_index('ix_device_codes_device_code', table_name='device_codes')
op.drop_table('device_codes')

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