mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5236c3094a | |||
| 2d8c0168ae | |||
| 989a4e662b | |||
| ecfbae2285 | |||
| c9cf351697 | |||
| aca568cfbe | |||
| 3366ad9de7 | |||
| f442e07b33 | |||
| fdf8b21b84 |
@@ -0,0 +1,23 @@
|
||||
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"}'
|
||||
@@ -87,8 +87,6 @@ VSCode Extension:
|
||||
|
||||
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
|
||||
|
||||
If you need to add labels when opening a PR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
These details may or may not be useful for your current task.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<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>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><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://dub.sh/openhands"><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://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord 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/>
|
||||
@@ -142,7 +142,7 @@ troubleshooting resources, and advanced configuration options.
|
||||
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 Discord or Github:
|
||||
|
||||
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Slack workspace](https://dub.sh/openhands) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
|
||||
- [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.
|
||||
|
||||
@@ -160,7 +160,7 @@ See the monthly OpenHands roadmap [here](https://github.com/orgs/All-Hands-AI/pr
|
||||
|
||||
## 📜 License
|
||||
|
||||
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
|
||||
Distributed under the MIT License, with the exception of the `enterprise/` folder. See [`LICENSE`](./LICENSE) for more information.
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
<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>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
|
||||
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></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="致谢"></a>
|
||||
<br/>
|
||||
@@ -107,7 +107,7 @@ docker run -it --rm --pull=always \
|
||||
OpenHands是一个社区驱动的项目,我们欢迎每个人的贡献。我们大部分沟通
|
||||
通过Slack进行,因此这是开始的最佳场所,但我们也很乐意您通过Discord或Github与我们联系:
|
||||
|
||||
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - 这里我们讨论研究、架构和未来发展。
|
||||
- [加入我们的Slack工作空间](https://dub.sh/openhands) - 这里我们讨论研究、架构和未来发展。
|
||||
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
|
||||
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<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>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
|
||||
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Discordコミュニティに参加"></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="クレジット"></a>
|
||||
<br/>
|
||||
|
||||
@@ -7,6 +7,7 @@ warn_unreachable = True
|
||||
warn_redundant_casts = True
|
||||
no_implicit_optional = True
|
||||
strict_optional = True
|
||||
disable_error_code = type-abstract
|
||||
|
||||
# Exclude third-party runtime directory from type checking
|
||||
exclude = (third_party/|enterprise/)
|
||||
|
||||
+30
-11
@@ -1,17 +1,36 @@
|
||||
# Setup
|
||||
# OpenHands Documentation
|
||||
|
||||
```
|
||||
This directory contains the documentation for OpenHands. The documentation is automatically synchronized with the [All-Hands-AI/docs](https://github.com/All-Hands-AI/docs) repository, which hosts the unified documentation site using Mintlify.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
The documentation files in this directory are automatically included in the main documentation site via Git submodules. When you make changes to documentation in this repository, they will be automatically synchronized to the docs repository.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Automatic Sync**: When documentation changes are pushed to the `main` branch, a GitHub Action automatically notifies the docs repository
|
||||
2. **Submodule Update**: The docs repository updates its submodule reference to include your latest changes
|
||||
3. **Site Rebuild**: Mintlify automatically rebuilds and deploys the documentation site
|
||||
|
||||
## Making Documentation Changes
|
||||
|
||||
Simply edit the documentation files in this directory as usual. The synchronization happens automatically when changes are merged to the main branch.
|
||||
|
||||
## Local Development
|
||||
|
||||
For local documentation development in this repository only:
|
||||
|
||||
```bash
|
||||
npm install -g mint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
# or
|
||||
yarn global add mint
|
||||
```
|
||||
|
||||
# Preview
|
||||
|
||||
```
|
||||
# Preview local changes
|
||||
mint dev
|
||||
```
|
||||
|
||||
For the complete unified documentation site, work with the [All-Hands-AI/docs](https://github.com/All-Hands-AI/docs) repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
The Mintlify configuration (`docs.json`) has been moved to the root of the [All-Hands-AI/docs](https://github.com/All-Hands-AI/docs) repository to enable unified documentation across multiple repositories.
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA",
|
||||
"slack": "https://dub.sh/openhands",
|
||||
"github": "https://github.com/All-Hands-AI/OpenHands",
|
||||
"discord": "https://discord.gg/ESHStjSjD4"
|
||||
}
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ If you would like to set things up more systematically, you can:
|
||||
1. **Search existing issues**: Check our [GitHub issues](https://github.com/All-Hands-AI/OpenHands/issues) to see if
|
||||
others have encountered the same problem.
|
||||
2. **Join our community**: Get help from other users and developers:
|
||||
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA)
|
||||
- [Slack community](https://dub.sh/openhands)
|
||||
- [Discord server](https://discord.gg/ESHStjSjD4)
|
||||
3. **Check our troubleshooting guide**: Common issues and solutions are documented in
|
||||
[Troubleshooting](/usage/troubleshooting/troubleshooting).
|
||||
|
||||
@@ -119,7 +119,7 @@ When started for the first time, OpenHands will prompt you to set up the LLM pro
|
||||
|
||||
That's it! You can now start using OpenHands with the local LLM server.
|
||||
|
||||
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) or [Discord](https://discord.gg/ESHStjSjD4).
|
||||
If you encounter any issues, let us know on [Slack](https://dub.sh/openhands) or [Discord](https://discord.gg/ESHStjSjD4).
|
||||
|
||||
## Advanced: Alternative LLM Backends
|
||||
|
||||
|
||||
+22
-10
@@ -1,10 +1,22 @@
|
||||
# Closed Source extension of Openhands proper (OSS)
|
||||
# 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).
|
||||
|
||||
The closed source (CSS) code in the `/app` directory builds on top of open source (OSS) code, extending its functionality. The CSS code is entangled with the OSS code in two ways
|
||||
> [!WARNING]
|
||||
> This is a work in progress and may contain bugs, incomplete features, or breaking changes.
|
||||
|
||||
- CSS stacks on top of OSS. For example, the middleware in CSS 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)
|
||||
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).
|
||||
|
||||
- CSS overrides the implementation in OSS (only one is present at a time). For example, the server config [`SaasServerConfig`](https://github.com/All-Hands-AI/deploy/blob/main/app/server/config.py#L43) 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))
|
||||
You may also want to check out the MIT-licensed [OpenHands](https://github.com/All-Hands-AI/OpenHands)
|
||||
|
||||
## Extension of OpenHands (OSS)
|
||||
|
||||
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
|
||||
|
||||
- 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 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))
|
||||
|
||||
Key areas that change on `SAAS` are
|
||||
|
||||
@@ -12,21 +24,21 @@ Key areas that change on `SAAS` are
|
||||
- User settings
|
||||
- etc
|
||||
|
||||
## Authentication
|
||||
### Authentication
|
||||
|
||||
| Aspect | OSS | CSS |
|
||||
| Aspect | OSS | 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 |
|
||||
| **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 |
|
||||
|
||||
Note that in the future, authentication will happen via keycloak. All modifications for authentication will happen in CSS.
|
||||
Note that in the future, authentication will happen via keycloak. All modifications for authentication will happen in enterprise.
|
||||
|
||||
## GitHub Service
|
||||
### GitHub Service
|
||||
|
||||
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 | CSS |
|
||||
| Aspect | OSS | Enterprise |
|
||||
| ------------------------- | -------------------------------------- | ---------------------------------------------- |
|
||||
| **Class used** | `GitHubService` | `SaaSGitHubService` |
|
||||
| **Token used** | User's PAT fetched from `Settings` | User's token fetched from `GitHubTokenManager` |
|
||||
@@ -39,6 +51,6 @@ 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
|
||||
- On CSS, the entire APP resolves around the Github User ID. This is because the cookie sets it, so `openhands/server` AND `deploy/app/server` depend on it and completly ignore `request.state.github_token` (token is fetched from `GithubTokenManager` instead)
|
||||
- On Enterprise, the entire APP resolves around the Github User ID. This is because the cookie sets it, so `openhands/server` AND `enterprise/server` depend on it and 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.
|
||||
|
||||
@@ -10,11 +10,14 @@ from experiments.experiment_versions import (
|
||||
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.server.session.conversation_init_data import ConversationInitData
|
||||
|
||||
|
||||
class SaaSExperimentManager(ExperimentManager):
|
||||
@staticmethod
|
||||
def run_conversation_variant_test(user_id, conversation_id, conversation_settings):
|
||||
def run_conversation_variant_test(
|
||||
user_id, conversation_id, conversation_settings
|
||||
) -> ConversationInitData:
|
||||
"""
|
||||
Run conversation variant test and potentially modify the conversation settings
|
||||
based on the PostHog feature flags.
|
||||
@@ -53,7 +56,7 @@ class SaaSExperimentManager(ExperimentManager):
|
||||
@staticmethod
|
||||
def run_config_variant_test(
|
||||
user_id: str, conversation_id: str, config: OpenHandsConfig
|
||||
):
|
||||
) -> OpenHandsConfig:
|
||||
"""
|
||||
Run agent config variant test and potentially modify the OpenHands config
|
||||
based on the current experiment type and PostHog feature flags.
|
||||
|
||||
@@ -14,9 +14,10 @@ from server.constants import (
|
||||
from storage.experiment_assignment_store import ExperimentAssignmentStore
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.session.conversation_init_data import ConversationInitData
|
||||
|
||||
|
||||
def _get_model_variant(user_id, conversation_id) -> str | None:
|
||||
def _get_model_variant(user_id: str | None, conversation_id: str) -> str | None:
|
||||
if not EXPERIMENT_CLAUDE4_VS_GPT5:
|
||||
logger.info(
|
||||
'experiment_manager:ab_testing:skipped',
|
||||
@@ -104,7 +105,11 @@ def _get_model_variant(user_id, conversation_id) -> str | None:
|
||||
return enabled_variant
|
||||
|
||||
|
||||
def handle_claude4_vs_gpt5_experiment(user_id, conversation_id, conversation_settings):
|
||||
def handle_claude4_vs_gpt5_experiment(
|
||||
user_id: str | None,
|
||||
conversation_id: str,
|
||||
conversation_settings: ConversationInitData,
|
||||
) -> ConversationInitData:
|
||||
"""
|
||||
Handle the LiteLLM model experiment.
|
||||
|
||||
@@ -120,7 +125,7 @@ def handle_claude4_vs_gpt5_experiment(user_id, conversation_id, conversation_set
|
||||
enabled_variant = _get_model_variant(user_id, conversation_id)
|
||||
|
||||
if not enabled_variant:
|
||||
return None
|
||||
return conversation_settings
|
||||
|
||||
# Set the model based on the feature flag variant
|
||||
if enabled_variant == 'gpt5':
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.server.session.conversation_init_data import ConversationInitData
|
||||
|
||||
|
||||
def _get_condenser_max_step_variant(user_id, conversation_id):
|
||||
@@ -114,8 +115,10 @@ def _get_condenser_max_step_variant(user_id, conversation_id):
|
||||
|
||||
|
||||
def handle_condenser_max_step_experiment(
|
||||
user_id: str, conversation_id: str, conversation_settings
|
||||
):
|
||||
user_id: str | None,
|
||||
conversation_id: str,
|
||||
conversation_settings: ConversationInitData,
|
||||
) -> ConversationInitData:
|
||||
"""
|
||||
Handle the condenser max step experiment for conversation settings.
|
||||
|
||||
|
||||
Generated
+93
-112
@@ -18,9 +18,9 @@
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/react-stripe-js": "^4.0.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tanstack/react-query": "^5.85.9",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.87.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -31,14 +31,14 @@
|
||||
"downshift": "^9.0.10",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.4.2",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.30",
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.261.6",
|
||||
"posthog-js": "^1.261.7",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -72,7 +72,7 @@
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
@@ -5526,26 +5526,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
|
||||
"integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
|
||||
"integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.5.1",
|
||||
"lightningcss": "1.30.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"magic-string": "^0.30.18",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.12"
|
||||
"tailwindcss": "4.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
|
||||
"integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
|
||||
"integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.4",
|
||||
"tar": "^7.4.3"
|
||||
@@ -5554,28 +5552,27 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.12",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.12",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.12",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.12",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.12",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.13",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.13",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.13",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.13",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.13",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
|
||||
"integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
|
||||
"integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -5585,13 +5582,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
|
||||
"integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
|
||||
"integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -5601,13 +5597,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
|
||||
"integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
|
||||
"integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -5617,13 +5612,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
|
||||
"integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
|
||||
"integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -5633,13 +5627,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
|
||||
"integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
|
||||
"integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5649,13 +5642,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
|
||||
"integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
|
||||
"integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5665,13 +5657,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
|
||||
"integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
|
||||
"integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5681,13 +5672,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
|
||||
"integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
|
||||
"integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5697,13 +5687,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
|
||||
"integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
|
||||
"integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5713,9 +5702,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
|
||||
"integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
|
||||
"integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -5727,7 +5716,6 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.5",
|
||||
@@ -5796,13 +5784,12 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
|
||||
"integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
|
||||
"integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -5812,13 +5799,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
|
||||
"integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
|
||||
"integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -5828,16 +5814,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz",
|
||||
"integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz",
|
||||
"integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.12",
|
||||
"@tailwindcss/oxide": "4.1.12",
|
||||
"@tailwindcss/node": "4.1.13",
|
||||
"@tailwindcss/oxide": "4.1.13",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.12"
|
||||
"tailwindcss": "4.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
@@ -5857,14 +5842,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz",
|
||||
"integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz",
|
||||
"integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.12",
|
||||
"@tailwindcss/oxide": "4.1.12",
|
||||
"tailwindcss": "4.1.12"
|
||||
"@tailwindcss/node": "4.1.13",
|
||||
"@tailwindcss/oxide": "4.1.13",
|
||||
"tailwindcss": "4.1.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
@@ -5887,20 +5871,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.85.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.9.tgz",
|
||||
"integrity": "sha512-5fxb9vwyftYE6KFLhhhDyLr8NO75+Wpu7pmTo+TkwKmMX2oxZDoLwcqGP8ItKSpUMwk3urWgQDZfyWr5Jm9LsQ==",
|
||||
"version": "5.87.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.0.tgz",
|
||||
"integrity": "sha512-gRZig2csRl71i/HEAHlE9TOmMqKKs9WkMAqIUlzagH+sNtgjvqxwaVo2HmfNGe+iDWUak0ratSkiRv0m/Y8ijg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.85.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.9.tgz",
|
||||
"integrity": "sha512-2T5zgSpcOZXGkH/UObIbIkGmUPQqZqn7esVQFXLOze622h4spgWf5jmvrqAo9dnI13/hyMcNsF1jsoDcb59nJQ==",
|
||||
"version": "5.87.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.0.tgz",
|
||||
"integrity": "sha512-3uRCGHo7KWHl6h7ptzLd5CbrjTQP5Q/37aC1cueClkSN4t/OaNFmfGolgs1AoA0kFjP/OZxTY2ytQoifyJzpWQ==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.85.9"
|
||||
"@tanstack/query-core": "5.87.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6164,11 +6148,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
|
||||
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
|
||||
"version": "24.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
|
||||
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
@@ -10707,9 +10690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.4.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz",
|
||||
"integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==",
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz",
|
||||
"integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -10724,7 +10707,6 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -14370,9 +14352,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.261.6",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.6.tgz",
|
||||
"integrity": "sha512-tson+4i+T2YkGYlj/oGjFwKRpBFqhM7Xr9ZmXGEtNFkZc6ZQHYCzObeeHT6BbKc5d/dAfMCPtvPCKssARaK6eQ==",
|
||||
"version": "1.261.7",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.7.tgz",
|
||||
"integrity": "sha512-Fjpbz6VfIMsEbKIN/UyTWhU1DGgVIngqoRjPGRolemIMOVzTfI77OZq8WwiBhMug+rU+wNhGCQhC41qRlR5CxA==",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.0.2",
|
||||
"core-js": "^3.38.1",
|
||||
@@ -16527,10 +16509,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
|
||||
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
|
||||
"license": "MIT"
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.3",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/react-stripe-js": "^4.0.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tanstack/react-query": "^5.85.9",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.87.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -30,14 +30,14 @@
|
||||
"downshift": "^9.0.10",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.4.2",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.30",
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.261.6",
|
||||
"posthog-js": "^1.261.7",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
|
||||
@@ -26,7 +26,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
* Use the `create_pr` tool to create a pull request, if you haven't already
|
||||
* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* If you need to add labels when opening a PR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
|
||||
* After opening or updating a pull request, send the user a short message with a link to the pull request.
|
||||
* Do NOT mark a pull request as ready to review unless the user explicitly says so
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
|
||||
@@ -26,7 +26,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
* Use the `create_mr` tool to create a merge request, if you haven't already
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* If you need to add labels when opening a MR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
|
||||
* After opening or updating a merge request, send the user a short message with a link to the merge request.
|
||||
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
|
||||
```bash
|
||||
|
||||
@@ -31,13 +31,15 @@ def load_experiment_config(conversation_id: str) -> ExperimentConfig | None:
|
||||
class ExperimentManager:
|
||||
@staticmethod
|
||||
def run_conversation_variant_test(
|
||||
user_id: str, conversation_id: str, conversation_settings: ConversationInitData
|
||||
user_id: str | None,
|
||||
conversation_id: str,
|
||||
conversation_settings: ConversationInitData,
|
||||
) -> ConversationInitData:
|
||||
return conversation_settings
|
||||
|
||||
@staticmethod
|
||||
def run_config_variant_test(
|
||||
user_id: str, conversation_id: str, config: OpenHandsConfig
|
||||
user_id: str | None, conversation_id: str, config: OpenHandsConfig
|
||||
) -> OpenHandsConfig:
|
||||
exp_config = load_experiment_config(conversation_id)
|
||||
if exp_config and exp_config.config:
|
||||
|
||||
@@ -335,6 +335,26 @@ class ProviderHandler:
|
||||
unique_repos.append(repo)
|
||||
return unique_repos
|
||||
|
||||
def _infer_provider_from_repo_name(self, repo_name: str) -> ProviderType:
|
||||
"""Infer the git provider from repository name or URL.
|
||||
|
||||
Args:
|
||||
repo_name: Repository name or URL
|
||||
|
||||
Returns:
|
||||
Inferred ProviderType, defaults to GitHub if cannot determine
|
||||
"""
|
||||
repo_lower = repo_name.lower()
|
||||
|
||||
# Check for provider domains in the repo name/URL
|
||||
if 'gitlab.com' in repo_lower or 'gitlab' in repo_lower:
|
||||
return ProviderType.GITLAB
|
||||
elif 'bitbucket.org' in repo_lower or 'bitbucket' in repo_lower:
|
||||
return ProviderType.BITBUCKET
|
||||
else:
|
||||
# Default to GitHub for unknown or github.com
|
||||
return ProviderType.GITHUB
|
||||
|
||||
async def set_event_stream_secrets(
|
||||
self,
|
||||
event_stream: EventStream,
|
||||
@@ -619,13 +639,24 @@ class ProviderHandler:
|
||||
Returns:
|
||||
Authenticated git URL if credentials are available, otherwise regular HTTPS URL
|
||||
"""
|
||||
# Initialize variables with defaults
|
||||
provider = self._infer_provider_from_repo_name(repo_name)
|
||||
# Keep the original repo_name as provided by default
|
||||
|
||||
try:
|
||||
repository = await self.verify_repo_provider(repo_name)
|
||||
# Update with verified information if successful
|
||||
provider = repository.git_provider
|
||||
repo_name = repository.full_name
|
||||
except AuthenticationError:
|
||||
raise Exception('Git provider authentication issue when getting remote URL')
|
||||
|
||||
provider = repository.git_provider
|
||||
repo_name = repository.full_name
|
||||
except Exception as e:
|
||||
# Handle network errors by falling back to public URL
|
||||
logger.warning(
|
||||
f'Repository verification failed (possibly offline): {e}. '
|
||||
f'Using public HTTPS URL for repository: {repo_name}'
|
||||
)
|
||||
# Use the inferred provider and original repo_name (already set above)
|
||||
|
||||
domain = self.PROVIDER_DOMAINS[provider]
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from openhands.integrations.provider import (
|
||||
ProviderHandler,
|
||||
)
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
CreateMicroagent,
|
||||
ProviderType,
|
||||
SuggestedTask,
|
||||
@@ -45,7 +46,6 @@ from openhands.server.services.conversation_service import (
|
||||
setup_init_conversation_settings,
|
||||
)
|
||||
from openhands.server.shared import (
|
||||
ConversationManagerImpl,
|
||||
ConversationStoreImpl,
|
||||
config,
|
||||
conversation_manager,
|
||||
@@ -244,7 +244,19 @@ async def new_conversation(
|
||||
if repository:
|
||||
provider_handler = ProviderHandler(provider_tokens)
|
||||
# Check against git_provider, otherwise check all provider apis
|
||||
await provider_handler.verify_repo_provider(repository, git_provider)
|
||||
# Only verify if we have valid provider tokens and can connect
|
||||
try:
|
||||
await provider_handler.verify_repo_provider(repository, git_provider)
|
||||
except AuthenticationError:
|
||||
# Re-raise authentication errors as they indicate invalid tokens
|
||||
raise
|
||||
except Exception as e:
|
||||
# Log network/connection errors but allow conversation to proceed
|
||||
# This enables offline usage when no network connectivity is available
|
||||
logger.warning(
|
||||
f'Repository verification failed (possibly offline): {e}. '
|
||||
f'Proceeding with conversation creation for repository: {repository}'
|
||||
)
|
||||
|
||||
conversation_id = getattr(data, 'conversation_id', None) or uuid.uuid4().hex
|
||||
agent_loop_info = await create_new_conversation(
|
||||
@@ -397,7 +409,7 @@ async def get_prompt(
|
||||
)
|
||||
|
||||
prompt_template = generate_prompt_template(stringified_events)
|
||||
prompt = generate_prompt(llm_config, prompt_template, conversation_id)
|
||||
prompt = await generate_prompt(llm_config, prompt_template, conversation_id)
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
@@ -413,7 +425,7 @@ def generate_prompt_template(events: str) -> str:
|
||||
return template.render(events=events)
|
||||
|
||||
|
||||
def generate_prompt(
|
||||
async def generate_prompt(
|
||||
llm_config: LLMConfig, prompt_template: str, conversation_id: str
|
||||
) -> str:
|
||||
messages = [
|
||||
@@ -427,7 +439,7 @@ def generate_prompt(
|
||||
},
|
||||
]
|
||||
|
||||
raw_prompt = ConversationManagerImpl.request_llm_completion(
|
||||
raw_prompt = await conversation_manager.request_llm_completion(
|
||||
'remember_prompt', conversation_id, llm_config, messages
|
||||
)
|
||||
prompt = re.search(r'<update_prompt>(.*?)</update_prompt>', raw_prompt, re.DOTALL)
|
||||
|
||||
@@ -91,7 +91,10 @@ async def create_pr(
|
||||
body: Annotated[str | None, Field(description='PR body')],
|
||||
draft: Annotated[bool, Field(description='Whether PR opened is a draft')] = True,
|
||||
labels: Annotated[
|
||||
list[str] | None, Field(description='Labels to apply to the PR')
|
||||
list[str] | None,
|
||||
Field(
|
||||
description='Optional labels to apply to the PR. If labels are provided, they must be selected from the repository’s existing labels. Do not invent new ones. If the repository’s labels are not known, fetch them first.'
|
||||
),
|
||||
] = None,
|
||||
) -> str:
|
||||
"""Open a PR in GitHub"""
|
||||
@@ -161,7 +164,10 @@ async def create_mr(
|
||||
],
|
||||
description: Annotated[str | None, Field(description='MR description')],
|
||||
labels: Annotated[
|
||||
list[str] | None, Field(description='Labels to apply to the MR')
|
||||
list[str] | None,
|
||||
Field(
|
||||
description='Optional labels to apply to the MR. If labels are provided, they must be selected from the repository’s existing labels. Do not invent new ones. If the repository’s labels are not known, fetch them first.'
|
||||
),
|
||||
] = None,
|
||||
) -> str:
|
||||
"""Open a MR in GitLab"""
|
||||
|
||||
@@ -31,6 +31,14 @@ def import_from(qual_name: str):
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def _get_impl(cls: type[T], impl_name: str | None) -> type[T]:
|
||||
if impl_name is None:
|
||||
return cls
|
||||
impl_class = import_from(impl_name)
|
||||
assert cls == impl_class or issubclass(impl_class, cls)
|
||||
return impl_class
|
||||
|
||||
|
||||
def get_impl(cls: type[T], impl_name: str | None) -> type[T]:
|
||||
"""Import and validate a named implementation of a base class.
|
||||
|
||||
@@ -62,8 +70,4 @@ def get_impl(cls: type[T], impl_name: str | None) -> type[T]:
|
||||
|
||||
The implementation is cached to avoid repeated imports of the same class.
|
||||
"""
|
||||
if impl_name is None:
|
||||
return cls
|
||||
impl_class = import_from(impl_name)
|
||||
assert cls == impl_class or issubclass(impl_class, cls)
|
||||
return impl_class
|
||||
return _get_impl(cls, impl_name) # type: ignore
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
# User ID Analysis Report - OpenHands Enterprise
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report provides a comprehensive analysis of all user_id occurrences and variations within the `/enterprise` directory of the OpenHands codebase. The analysis reveals **1,451 total occurrences** of user_id-related identifiers across **130+ files**, serving multiple critical purposes in authentication, authorization, resource scoping, and cross-platform user linking.
|
||||
|
||||
## User ID Variations and Counts
|
||||
|
||||
| User ID Type | Count | Purpose |
|
||||
|--------------|-------|---------|
|
||||
| `user_id` | 1,451 | Primary user identifier, authentication, resource scoping |
|
||||
| `keycloak_user_id` | 249 | Identity provider linking, authentication backend |
|
||||
| `slack_user_id` | 43 | Slack integration user mapping |
|
||||
| `jira_user_id` | 27 | Jira integration user mapping |
|
||||
| `linear_user_id` | 27 | Linear integration user mapping |
|
||||
| `github_user_id` | 25 | GitHub integration user mapping |
|
||||
| `gitlab_user_id` | 0 | GitLab integration (not currently used) |
|
||||
| `bitbucket_user_id` | 0 | Bitbucket integration (not currently used) |
|
||||
|
||||
**Total Occurrences: 1,822**
|
||||
|
||||
## Primary User ID Purposes
|
||||
|
||||
### 1. Authentication & Authorization (Primary Auth)
|
||||
- **Files**: `enterprise/server/auth/saas_user_auth.py`, `enterprise/server/auth/token_manager.py`
|
||||
- **Purpose**: Core authentication mechanism using Keycloak as identity provider
|
||||
- **Key Functions**:
|
||||
- `get_user_id()` - Primary user identification
|
||||
- Token validation and refresh
|
||||
- Session management
|
||||
- Access control
|
||||
|
||||
### 2. Resource Scoping & Data Isolation
|
||||
- **Files**: `enterprise/storage/saas_conversation_store.py`, `enterprise/storage/api_key_store.py`
|
||||
- **Purpose**: Ensure users can only access their own resources
|
||||
- **Key Patterns**:
|
||||
```python
|
||||
# Conversation scoping
|
||||
.filter(StoredConversationMetadata.user_id == self.user_id)
|
||||
|
||||
# API key scoping
|
||||
.filter(ApiKey.user_id == user_id)
|
||||
```
|
||||
|
||||
### 3. Cross-Platform User Linking
|
||||
- **Files**: Integration storage models (`slack_user.py`, `jira_user.py`, `linear_user.py`)
|
||||
- **Purpose**: Link OpenHands users to external platform accounts
|
||||
- **Pattern**:
|
||||
```python
|
||||
class SlackUser(Base):
|
||||
keycloak_user_id = Column(String, nullable=False, index=True)
|
||||
slack_user_id = Column(String, nullable=False, index=True)
|
||||
```
|
||||
|
||||
### 4. Billing & Usage Tracking
|
||||
- **Files**: `enterprise/server/routes/billing.py`, `enterprise/storage/billing_session.py`
|
||||
- **Purpose**: Track usage and billing per user
|
||||
- **Key Functions**:
|
||||
- Credit calculation
|
||||
- Usage monitoring
|
||||
- Payment method management
|
||||
|
||||
### 5. Settings & Preferences Management
|
||||
- **Files**: `enterprise/storage/user_settings.py`, `enterprise/storage/saas_settings_store.py`
|
||||
- **Purpose**: Store user-specific configuration
|
||||
- **Key Settings**:
|
||||
- LLM preferences
|
||||
- Security settings
|
||||
- UI preferences
|
||||
- Integration configurations
|
||||
|
||||
## API Route Analysis - Depends Parameters
|
||||
|
||||
### Authentication Dependencies
|
||||
The following `Depends` parameters are used across API routes for user authentication:
|
||||
|
||||
| Dependency | Purpose | Usage Count |
|
||||
|------------|---------|-------------|
|
||||
| `get_user_id` | Extract authenticated user ID | 25+ routes |
|
||||
| `get_access_token` | Get OAuth access token | 15+ routes |
|
||||
| `get_provider_tokens` | Get integration tokens | 15+ routes |
|
||||
|
||||
### Key API Route Patterns
|
||||
|
||||
#### 1. User Resource Routes (`/enterprise/server/routes/user.py`)
|
||||
```python
|
||||
async def saas_get_user_repositories(
|
||||
user_id: str | None = Depends(get_user_id),
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
access_token: SecretStr | None = Depends(get_access_token),
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. Billing Routes (`/enterprise/server/routes/billing.py`)
|
||||
```python
|
||||
async def get_credits(user_id: str = Depends(get_user_id)) -> GetCreditsResponse:
|
||||
async def has_payment_method(user_id: str = Depends(get_user_id)) -> bool:
|
||||
```
|
||||
|
||||
#### 3. API Key Management (`/enterprise/server/routes/api_keys.py`)
|
||||
```python
|
||||
async def create_api_key(key_data: ApiKeyCreate, user_id: str = Depends(get_user_id)):
|
||||
async def list_api_keys(user_id: str = Depends(get_user_id)):
|
||||
```
|
||||
|
||||
#### 4. Integration Routes
|
||||
- **Slack**: `enterprise/server/routes/integration/slack.py`
|
||||
- **Jira**: `enterprise/server/routes/integration/jira.py`
|
||||
- **Linear**: `enterprise/server/routes/integration/linear.py`
|
||||
- **Jira DC**: `enterprise/server/routes/integration/jira_dc.py`
|
||||
|
||||
## Database Schema Analysis
|
||||
|
||||
### Core User Tables
|
||||
|
||||
#### 1. User Settings (`user_settings`)
|
||||
```sql
|
||||
CREATE TABLE user_settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
keycloak_user_id VARCHAR INDEX, -- Primary user identifier
|
||||
language VARCHAR,
|
||||
agent VARCHAR,
|
||||
llm_model VARCHAR,
|
||||
-- ... 30+ user preference columns
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. API Keys (`api_keys`)
|
||||
```sql
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id VARCHAR(255) INDEX, -- Links to keycloak_user_id
|
||||
key VARCHAR(255) UNIQUE,
|
||||
name VARCHAR(255),
|
||||
created_at DATETIME,
|
||||
expires_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
### Integration User Mapping Tables
|
||||
|
||||
#### 1. Slack Users (`slack_users`)
|
||||
```sql
|
||||
CREATE TABLE slack_users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
keycloak_user_id VARCHAR INDEX,
|
||||
slack_user_id VARCHAR INDEX,
|
||||
slack_display_name VARCHAR
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. Jira Users (`jira_users`)
|
||||
```sql
|
||||
CREATE TABLE jira_users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
keycloak_user_id VARCHAR INDEX,
|
||||
jira_user_id VARCHAR INDEX,
|
||||
jira_workspace_id INTEGER INDEX
|
||||
);
|
||||
```
|
||||
|
||||
#### 3. Linear Users (`linear_users`)
|
||||
```sql
|
||||
CREATE TABLE linear_users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
keycloak_user_id VARCHAR INDEX,
|
||||
linear_user_id VARCHAR INDEX,
|
||||
linear_workspace_id INTEGER INDEX
|
||||
);
|
||||
```
|
||||
|
||||
## Security & Data Flow Analysis
|
||||
|
||||
### 1. Authentication Flow
|
||||
```
|
||||
1. User authenticates via Keycloak OAuth
|
||||
2. Keycloak returns JWT with `sub` claim (keycloak_user_id)
|
||||
3. OpenHands extracts user_id from JWT
|
||||
4. All subsequent requests use user_id for authorization
|
||||
```
|
||||
|
||||
### 2. Resource Access Pattern
|
||||
```
|
||||
API Request → Authentication Middleware → Extract user_id →
|
||||
Database Query with user_id filter → Return user-scoped data
|
||||
```
|
||||
|
||||
### 3. Integration Linking Flow
|
||||
```
|
||||
1. User authenticates with external service (Slack/Jira/Linear)
|
||||
2. External service returns platform-specific user_id
|
||||
3. OpenHands stores mapping: keycloak_user_id ↔ platform_user_id
|
||||
4. Future requests use mapping for cross-platform operations
|
||||
```
|
||||
|
||||
## File Distribution Analysis
|
||||
|
||||
### High-Density User ID Files (>20 occurrences)
|
||||
1. `enterprise/storage/saas_conversation_store.py` - 45 occurrences
|
||||
2. `enterprise/server/auth/token_manager.py` - 38 occurrences
|
||||
3. `enterprise/storage/saas_settings_store.py` - 35 occurrences
|
||||
4. `enterprise/server/routes/billing.py` - 32 occurrences
|
||||
5. `enterprise/storage/api_key_store.py` - 28 occurrences
|
||||
|
||||
### Integration-Specific Files
|
||||
- **Slack Integration**: 15 files, 43 total occurrences
|
||||
- **Jira Integration**: 12 files, 27 total occurrences
|
||||
- **Linear Integration**: 10 files, 27 total occurrences
|
||||
- **GitHub Integration**: 8 files, 25 total occurrences
|
||||
|
||||
### Test Files
|
||||
- **Unit Tests**: 25 test files with user_id usage
|
||||
- **Integration Tests**: 8 test files for platform integrations
|
||||
- **Mock Services**: 3 files for testing user authentication
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
### 1. User-Scoped Data Access
|
||||
All database queries include user_id filtering to ensure data isolation:
|
||||
```python
|
||||
def get_user_conversations(self, user_id: str):
|
||||
return session.query(Conversation).filter(
|
||||
Conversation.user_id == user_id
|
||||
).all()
|
||||
```
|
||||
|
||||
### 2. Multi-Tenant Resource Management
|
||||
Resources are partitioned by user_id to support multi-tenancy:
|
||||
- Conversations
|
||||
- API Keys
|
||||
- Settings
|
||||
- Billing sessions
|
||||
- Integration configurations
|
||||
|
||||
### 3. Cross-Platform Identity Resolution
|
||||
User identities are resolved across platforms using mapping tables:
|
||||
```python
|
||||
def get_slack_user_id(keycloak_user_id: str) -> str:
|
||||
slack_user = session.query(SlackUser).filter(
|
||||
SlackUser.keycloak_user_id == keycloak_user_id
|
||||
).first()
|
||||
return slack_user.slack_user_id if slack_user else None
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 1. Security Enhancements
|
||||
- Implement consistent user_id validation across all routes
|
||||
- Add audit logging for user_id-based access patterns
|
||||
- Consider implementing user_id encryption for sensitive operations
|
||||
|
||||
### 2. Code Consistency
|
||||
- Standardize user_id parameter naming across all functions
|
||||
- Implement consistent error handling for invalid user_id values
|
||||
- Add type hints for all user_id parameters
|
||||
|
||||
### 3. Performance Optimizations
|
||||
- Add database indexes on all user_id columns
|
||||
- Implement caching for frequently accessed user data
|
||||
- Consider user_id-based database sharding for scalability
|
||||
|
||||
### 4. Integration Improvements
|
||||
- Implement GitLab and Bitbucket user_id support (currently unused)
|
||||
- Add user_id validation for all integration endpoints
|
||||
- Standardize integration user mapping patterns
|
||||
|
||||
### 5. Applying the "Linus Philosophy" to User ID Architecture 🐧
|
||||
|
||||
*In OpenHands, our AI agents follow a "Linus prompt" that embodies Linus Torvalds' programming philosophy, including the principle: **"Good code has no special cases"***
|
||||
|
||||
**Current User ID Special Cases Analysis:**
|
||||
- **Business Logic Branches**: Authentication flows, resource scoping, cross-platform linking
|
||||
- **Design Patches**: Multiple user_id types (`keycloak_user_id`, `slack_user_id`, etc.) handling platform-specific quirks
|
||||
- **Potential Simplification**: Consider a unified `UserIdentity` abstraction that encapsulates all platform-specific IDs
|
||||
|
||||
**Linus-Inspired Refactoring Opportunities:**
|
||||
```python
|
||||
# Current: Multiple special cases
|
||||
if platform == "slack":
|
||||
user_id = slack_user.slack_user_id
|
||||
elif platform == "jira":
|
||||
user_id = jira_user.jira_user_id
|
||||
elif platform == "linear":
|
||||
user_id = linear_user.linear_user_id
|
||||
|
||||
# Linus-approved: No special cases
|
||||
user_identity = UserIdentity(keycloak_user_id)
|
||||
platform_id = user_identity.get_platform_id(platform)
|
||||
```
|
||||
|
||||
*Even our AI agents would approve of cleaner user_id architecture! 🤖*
|
||||
|
||||
## Conclusion
|
||||
|
||||
The user_id system in OpenHands Enterprise serves as the foundational element for:
|
||||
- **Authentication**: Primary user identification via Keycloak
|
||||
- **Authorization**: Resource access control and data scoping
|
||||
- **Integration**: Cross-platform user identity management
|
||||
- **Multi-tenancy**: Secure data isolation between users
|
||||
- **Billing**: Usage tracking and payment management
|
||||
|
||||
The system demonstrates a well-architected approach to user management with consistent patterns across 130+ files and 1,822 total user_id references. The primary areas for improvement include enhanced security validation, performance optimization, and expanded integration support.
|
||||
|
||||
---
|
||||
|
||||
*Report generated on 2025-09-04*
|
||||
*Total files analyzed: 130+*
|
||||
*Total user_id occurrences: 1,822*
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Tests for provider offline functionality and variable scope issues."""
|
||||
|
||||
from types import MappingProxyType
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.integrations.provider import ProviderHandler, ProviderToken, ProviderType
|
||||
from openhands.integrations.service_types import AuthenticationError
|
||||
|
||||
|
||||
class TestProviderOfflineFunctionality:
|
||||
"""Test offline functionality and variable scope in ProviderHandler."""
|
||||
|
||||
@pytest.fixture
|
||||
def provider_handler(self):
|
||||
"""Create a ProviderHandler instance for testing."""
|
||||
tokens = MappingProxyType(
|
||||
{
|
||||
ProviderType.GITHUB: ProviderToken(token='test_token'),
|
||||
ProviderType.GITLAB: ProviderToken(token='gitlab_token'),
|
||||
}
|
||||
)
|
||||
return ProviderHandler(provider_tokens=tokens)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_authenticated_git_url_network_error_handling(
|
||||
self, provider_handler
|
||||
):
|
||||
"""Test that network errors are properly handled with fallback to inferred provider.
|
||||
|
||||
After the fix, variables are properly initialized before the try block,
|
||||
ensuring they're always available regardless of which exception path is taken.
|
||||
"""
|
||||
repo_name = 'test-owner/test-repo'
|
||||
|
||||
# Mock verify_repo_provider to raise a non-AuthenticationError exception
|
||||
# This simulates a network error or other exception during offline operation
|
||||
with patch.object(provider_handler, 'verify_repo_provider') as mock_verify:
|
||||
# Simulate a network error (not AuthenticationError)
|
||||
mock_verify.side_effect = ConnectionError('Network unreachable')
|
||||
|
||||
# After the fix, this should work correctly with proper variable initialization
|
||||
result = await provider_handler.get_authenticated_git_url(repo_name)
|
||||
|
||||
# Should return a GitHub URL with token (inferred from repo name)
|
||||
assert result == 'https://test_token@github.com/test-owner/test-repo.git'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_authenticated_git_url_proper_variable_scope(
|
||||
self, provider_handler
|
||||
):
|
||||
"""Test that verifies the variables are properly scoped after the fix.
|
||||
|
||||
This test ensures that after fixing the code structure, the variables
|
||||
'provider' and 'repo_name' are properly initialized and available
|
||||
regardless of which exception path is taken.
|
||||
"""
|
||||
repo_name = 'test-owner/test-repo'
|
||||
|
||||
# Test with network error - should use inferred provider and original repo_name
|
||||
with patch.object(provider_handler, 'verify_repo_provider') as mock_verify:
|
||||
mock_verify.side_effect = ConnectionError('Network unreachable')
|
||||
|
||||
result = await provider_handler.get_authenticated_git_url(repo_name)
|
||||
|
||||
# Should return authenticated URL with inferred GitHub provider
|
||||
assert result == 'https://test_token@github.com/test-owner/test-repo.git'
|
||||
|
||||
# Test with successful verification - should use verified provider and repo_name
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.git_provider = ProviderType.GITLAB
|
||||
mock_repository.full_name = 'verified-owner/verified-repo'
|
||||
|
||||
with patch.object(provider_handler, 'verify_repo_provider') as mock_verify:
|
||||
mock_verify.return_value = mock_repository
|
||||
|
||||
result = await provider_handler.get_authenticated_git_url(repo_name)
|
||||
|
||||
# Should return authenticated GitLab URL with verified details
|
||||
assert (
|
||||
result
|
||||
== 'https://oauth2:gitlab_token@gitlab.com/verified-owner/verified-repo.git'
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_authenticated_git_url_auth_error_handling(
|
||||
self, provider_handler
|
||||
):
|
||||
"""Test that AuthenticationError is properly handled and re-raised."""
|
||||
repo_name = 'test-owner/test-repo'
|
||||
|
||||
# Mock verify_repo_provider to raise AuthenticationError
|
||||
with patch.object(provider_handler, 'verify_repo_provider') as mock_verify:
|
||||
mock_verify.side_effect = AuthenticationError('Invalid token')
|
||||
|
||||
# AuthenticationError should be re-raised as a generic Exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await provider_handler.get_authenticated_git_url(repo_name)
|
||||
|
||||
assert 'Git provider authentication issue when getting remote URL' in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_authenticated_git_url_successful_case(self, provider_handler):
|
||||
"""Test the successful case where repository verification works."""
|
||||
repo_name = 'test-owner/test-repo'
|
||||
|
||||
# Mock a successful repository verification
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.git_provider = ProviderType.GITHUB
|
||||
mock_repository.full_name = 'test-owner/test-repo'
|
||||
|
||||
with patch.object(provider_handler, 'verify_repo_provider') as mock_verify:
|
||||
mock_verify.return_value = mock_repository
|
||||
|
||||
result = await provider_handler.get_authenticated_git_url(repo_name)
|
||||
|
||||
# Should return an authenticated GitHub URL
|
||||
assert result == 'https://test_token@github.com/test-owner/test-repo.git'
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Test offline conversation creation functionality."""
|
||||
|
||||
|
||||
def test_offline_repository_verification_logic():
|
||||
"""Test the logic for handling offline repository verification.
|
||||
|
||||
This test validates that our fix correctly handles different exception types:
|
||||
- AuthenticationError should be re-raised (invalid tokens)
|
||||
- Other exceptions should be logged and ignored (network issues)
|
||||
"""
|
||||
|
||||
# Define a mock AuthenticationError for testing
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
# Test case 1: AuthenticationError should be re-raised
|
||||
def test_auth_error_handling():
|
||||
"""Simulate the exception handling logic in our fix."""
|
||||
try:
|
||||
# Simulate AuthenticationError from repository verification
|
||||
raise AuthenticationError('Invalid token')
|
||||
except AuthenticationError:
|
||||
# This should be re-raised
|
||||
return 'auth_error_reraised'
|
||||
except Exception:
|
||||
# This should not be reached for AuthenticationError
|
||||
return 'other_error_ignored'
|
||||
|
||||
# Test case 2: Network errors should be ignored
|
||||
def test_network_error_handling():
|
||||
"""Simulate the exception handling logic in our fix."""
|
||||
try:
|
||||
# Simulate network error from repository verification
|
||||
raise Exception('Network unreachable')
|
||||
except Exception as e:
|
||||
# Check if it's an AuthenticationError
|
||||
if isinstance(e, AuthenticationError):
|
||||
return 'auth_error_reraised'
|
||||
else:
|
||||
# Log and ignore other errors (network issues)
|
||||
return 'network_error_ignored'
|
||||
|
||||
# Run the tests
|
||||
assert test_auth_error_handling() == 'auth_error_reraised'
|
||||
assert test_network_error_handling() == 'network_error_ignored'
|
||||
|
||||
|
||||
def test_repository_verification_skip_logic():
|
||||
"""Test that repository verification can be skipped when appropriate."""
|
||||
|
||||
# Define a mock AuthenticationError for testing
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
def simulate_conversation_creation_with_repo(
|
||||
repository, has_network_error=False, has_auth_error=False
|
||||
):
|
||||
"""Simulate the conversation creation logic with our fix."""
|
||||
if repository:
|
||||
# Simulate provider handler creation
|
||||
# provider_handler = ProviderHandler(provider_tokens)
|
||||
|
||||
try:
|
||||
# Simulate repository verification
|
||||
if has_auth_error:
|
||||
raise AuthenticationError('Invalid token')
|
||||
elif has_network_error:
|
||||
raise Exception('Network unreachable')
|
||||
else:
|
||||
# Successful verification
|
||||
pass
|
||||
except Exception as e:
|
||||
if isinstance(e, AuthenticationError):
|
||||
# Re-raise authentication errors
|
||||
raise
|
||||
else:
|
||||
# Log and ignore network errors
|
||||
print(
|
||||
f'Repository verification failed (possibly offline): {e}. Proceeding with conversation creation.'
|
||||
)
|
||||
|
||||
# Continue with conversation creation
|
||||
return 'conversation_created'
|
||||
|
||||
# Test successful verification
|
||||
result = simulate_conversation_creation_with_repo(
|
||||
'test/repo', has_network_error=False, has_auth_error=False
|
||||
)
|
||||
assert result == 'conversation_created'
|
||||
|
||||
# Test network error (should proceed)
|
||||
result = simulate_conversation_creation_with_repo(
|
||||
'test/repo', has_network_error=True, has_auth_error=False
|
||||
)
|
||||
assert result == 'conversation_created'
|
||||
|
||||
# Test authentication error (should raise)
|
||||
try:
|
||||
simulate_conversation_creation_with_repo(
|
||||
'test/repo', has_network_error=False, has_auth_error=True
|
||||
)
|
||||
raise AssertionError('Should have raised AuthenticationError')
|
||||
except AuthenticationError:
|
||||
pass # Expected
|
||||
|
||||
# Test no repository (should proceed)
|
||||
result = simulate_conversation_creation_with_repo(None)
|
||||
assert result == 'conversation_created'
|
||||
|
||||
|
||||
def test_provider_inference_logic():
|
||||
"""Test the provider inference logic for offline scenarios."""
|
||||
|
||||
# Mock the ProviderType enum
|
||||
class ProviderType:
|
||||
GITHUB = 'github'
|
||||
GITLAB = 'gitlab'
|
||||
BITBUCKET = 'bitbucket'
|
||||
|
||||
def infer_provider_from_repo_name(repo_name: str):
|
||||
"""Simulate the provider inference logic."""
|
||||
repo_lower = repo_name.lower()
|
||||
|
||||
# Check for provider domains in the repo name/URL
|
||||
if 'gitlab.com' in repo_lower or 'gitlab' in repo_lower:
|
||||
return ProviderType.GITLAB
|
||||
elif 'bitbucket.org' in repo_lower or 'bitbucket' in repo_lower:
|
||||
return ProviderType.BITBUCKET
|
||||
else:
|
||||
# Default to GitHub for unknown or github.com
|
||||
return ProviderType.GITHUB
|
||||
|
||||
# Test various repository name formats
|
||||
assert infer_provider_from_repo_name('owner/repo') == ProviderType.GITHUB
|
||||
assert (
|
||||
infer_provider_from_repo_name('https://github.com/owner/repo')
|
||||
== ProviderType.GITHUB
|
||||
)
|
||||
assert (
|
||||
infer_provider_from_repo_name('https://gitlab.com/owner/repo')
|
||||
== ProviderType.GITLAB
|
||||
)
|
||||
assert (
|
||||
infer_provider_from_repo_name('https://bitbucket.org/owner/repo')
|
||||
== ProviderType.BITBUCKET
|
||||
)
|
||||
assert infer_provider_from_repo_name('gitlab-owner/repo') == ProviderType.GITLAB
|
||||
assert (
|
||||
infer_provider_from_repo_name('bitbucket-owner/repo') == ProviderType.BITBUCKET
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_offline_repository_verification_logic()
|
||||
test_repository_verification_skip_logic()
|
||||
test_provider_inference_logic()
|
||||
print(
|
||||
'✅ All tests passed! Offline conversation creation logic is working correctly.'
|
||||
)
|
||||
Reference in New Issue
Block a user