Compare commits

..

6 Commits

Author SHA1 Message Date
Xingyao Wang 5bc6100694 Merge branch 'main' into enable-subagent-task-tool 2026-04-25 10:42:15 -04:00
openhands 3121e03029 refactor: pass enable_sub_agents through get_default_tools()
Remove direct TaskToolSet import and manual append. Instead pass
the user setting to get_default_tools() which now handles the
conditional inclusion internally.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-25 14:35:49 +00:00
Engel Nyst dd7234d712 ci: run PR review on fork PRs (#14109)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-25 03:20:34 +02:00
Juan Michelini 2a6f5c8976 feat: Auto-forward LMNR_* environment variables to agent-server (#14123)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-24 20:49:48 -03:00
openhands 90b5993c31 feat: gate sub-agent TaskToolSet behind enable_sub_agents setting
Instead of unconditionally appending TaskToolSet to the tools list,
check user.agent_settings.enable_sub_agents (default: False). This
allows sub-agent delegation to be rolled out gradually — users opt
in via their Settings page, and the feature is off by default.

Requires SDK >= next release (adds enable_sub_agents to LLMAgentSettings).

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-24 22:49:40 +00:00
openhands a08580f991 feat: enable sub-agent delegation via TaskToolSet in app server
Add TaskToolSet to the default tools list so that the main agent can
delegate tasks to built-in sub-agents (bash-runner, code-explorer,
general-purpose, web-researcher) during conversations.

The sub-agent infrastructure is already fully implemented in the SDK:
- TaskToolSet and TaskManager handle sub-agent lifecycle
- Built-in sub-agent definitions are registered in the agent_server
- Sub-agents inherit the parent's LLM config and run in-process

The only missing piece was including the task tool in the app server's
default tool set.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-24 20:12:27 +00:00
8 changed files with 113 additions and 19 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: "pip"
+2 -2
View File
@@ -47,7 +47,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: "pip"
@@ -64,7 +64,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12
cache: "pip"
+31 -9
View File
@@ -2,12 +2,14 @@
name: PR Review by OpenHands
on:
# TEMPORARY MITIGATION (Clinejection hardening)
#
# We temporarily avoid `pull_request_target` here. We'll restore it after the PR review
# workflow is fully hardened for untrusted execution.
# Use pull_request for same-repo PRs so workflow changes can self-verify in PRs.
pull_request:
types: [opened, ready_for_review, labeled, review_requested]
# Use pull_request_target for fork PRs.
# The bot token used here is intentionally scoped to PR review operations,
# so the remaining blast radius is bounded even though PR content is untrusted.
pull_request_target:
types: [opened, ready_for_review, labeled, review_requested]
permissions:
contents: read
@@ -16,13 +18,33 @@ permissions:
jobs:
pr-review:
# Note: fork PRs will not have access to repository secrets under `pull_request`.
# Skip forks to avoid noisy failures until we restore a hardened `pull_request_target` flow.
# Run on same-repo PRs via pull_request and on fork PRs via pull_request_target.
# Trigger when one of the following conditions is met:
# 1. A new non-draft PR is opened by a non-first-time contributor, OR
# 2. A draft PR is converted to ready for review by a non-first-time contributor, OR
# 3. The 'review-this' label is added, OR
# 4. openhands-agent or all-hands-bot is requested as a reviewer
# Note: FIRST_TIME_CONTRIBUTOR and NONE PRs require manual trigger via label/reviewer request.
# Trigger logic:
# 1. Route same-repo PRs through `pull_request` and fork PRs through `pull_request_target`
# 2. Auto-trigger on `opened` / `ready_for_review` for non-first-time contributors
# 3. Always allow manual triggers via `review-this` or reviewer request
# The author association check is duplicated intentionally for both
# auto-triggered actions (`opened` and `ready_for_review`).
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
(
(github.event.action == 'opened' && github.event.pull_request.draft == false) ||
github.event.action == 'ready_for_review' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
) ||
(
github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository
)
) &&
(
(github.event.action == 'opened' && github.event.pull_request.draft == false && github.event.pull_request.author_association != 'FIRST_TIME_CONTRIBUTOR' && github.event.pull_request.author_association != 'NONE') ||
(github.event.action == 'ready_for_review' && github.event.pull_request.author_association != 'FIRST_TIME_CONTRIBUTOR' && github.event.pull_request.author_association != 'NONE') ||
(github.event.action == 'labeled' && github.event.label.name == 'review-this') ||
(
github.event.action == 'review_requested' &&
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
@@ -80,7 +80,7 @@ jobs:
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli') && !startsWith(github.ref, 'refs/tags/cloud-'))
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Poetry
@@ -1304,7 +1304,10 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
plan_path = self._compute_plan_path(project_dir, git_provider)
tools = get_planning_tools(plan_path=plan_path)
else:
tools = get_default_tools(enable_browser=True)
tools = get_default_tools(
enable_browser=True,
enable_sub_agents=user.agent_settings.enable_sub_agents,
)
# --- build AgentSettings and create agent ---------------------------
from fastmcp.mcp_config import MCPConfig
@@ -71,7 +71,7 @@ def get_agent_server_image() -> str:
# Prefixes for environment variables that should be auto-forwarded to agent-server
# These are typically configuration variables that affect the agent's behavior
AUTO_FORWARD_PREFIXES = ('LLM_',)
AUTO_FORWARD_PREFIXES = ('LLM_', 'LMNR_')
def get_agent_server_env() -> dict[str, str]:
@@ -80,9 +80,10 @@ def get_agent_server_env() -> dict[str, str]:
This function combines two sources of environment variables:
1. **Auto-forwarded variables**: Environment variables with certain prefixes
(e.g., LLM_*) are automatically forwarded to the agent-server container.
(e.g., LLM_*, LMNR_*) are automatically forwarded to the agent-server container.
This ensures that LLM configuration like timeouts and retry settings
work correctly in the two-container V1 architecture.
work correctly in the two-container V1 architecture, as well as
Laminar monitoring/analytics configuration.
2. **Explicit overrides via OH_AGENT_SERVER_ENV**: A JSON string that allows
setting arbitrary environment variables in the agent-server container.
@@ -90,6 +91,7 @@ def get_agent_server_env() -> dict[str, str]:
Auto-forwarded prefixes:
- LLM_* : LLM configuration (timeout, retries, model settings, etc.)
- LMNR_* : Laminar monitoring/analytics configuration
Usage:
# Auto-forwarding (no action needed):
@@ -97,6 +99,11 @@ def get_agent_server_env() -> dict[str, str]:
export LLM_NUM_RETRIES=10
# These will automatically be available in the agent-server
# Auto-forwarding for Laminar:
export LMNR_PROJECT_API_KEY=your-api-key
export LMNR_BASE_URL=https://app.lmnr.ai
# These will automatically be available in the agent-server
# Explicit override via JSON:
OH_AGENT_SERVER_ENV='{"DEBUG": "true", "CUSTOM_VAR": "value"}'
@@ -295,6 +295,68 @@ class TestLLMAutoForwarding:
assert 'Llm_Timeout' not in result
class TestLMNRAutoForwarding:
"""Test cases for automatic forwarding of LMNR_* environment variables."""
def test_auto_forward_prefixes_contains_lmnr(self):
"""Test that LMNR_ is in the auto-forward prefixes."""
assert 'LMNR_' in AUTO_FORWARD_PREFIXES
def test_lmnr_project_api_key_auto_forwarded(self):
"""Test that LMNR_PROJECT_API_KEY is automatically forwarded."""
env_vars = {
'LMNR_PROJECT_API_KEY': 'sk-test-key-12345',
'OTHER_VAR': 'should_not_be_included',
}
with patch.dict(os.environ, env_vars, clear=True):
result = get_agent_server_env()
assert 'LMNR_PROJECT_API_KEY' in result
assert result['LMNR_PROJECT_API_KEY'] == 'sk-test-key-12345'
assert 'OTHER_VAR' not in result
def test_lmnr_base_url_auto_forwarded(self):
"""Test that LMNR_BASE_URL is automatically forwarded."""
env_vars = {
'LMNR_BASE_URL': 'https://app.lmnr.ai',
}
with patch.dict(os.environ, env_vars, clear=True):
result = get_agent_server_env()
assert 'LMNR_BASE_URL' in result
assert result['LMNR_BASE_URL'] == 'https://app.lmnr.ai'
def test_multiple_lmnr_vars_auto_forwarded(self):
"""Test that multiple LMNR_* variables are automatically forwarded."""
env_vars = {
'LMNR_PROJECT_API_KEY': 'sk-test-key-12345',
'LMNR_BASE_URL': 'https://app.lmnr.ai',
'LMNR_VERBOSITY': 'debug',
}
with patch.dict(os.environ, env_vars, clear=True):
result = get_agent_server_env()
assert result['LMNR_PROJECT_API_KEY'] == 'sk-test-key-12345'
assert result['LMNR_BASE_URL'] == 'https://app.lmnr.ai'
assert result['LMNR_VERBOSITY'] == 'debug'
def test_lmnr_prefix_is_case_sensitive(self):
"""Test that LMNR_ prefix matching is case-sensitive."""
env_vars = {
'LMNR_PROJECT_API_KEY': 'sk-test-key', # Should be included
'lmnr_project_api_key': 'lowercase', # Should NOT be included
'Lmnr_Project_Api_Key': 'mixed', # Should NOT be included
}
with patch.dict(os.environ, env_vars, clear=True):
result = get_agent_server_env()
assert 'LMNR_PROJECT_API_KEY' in result
assert result['LMNR_PROJECT_API_KEY'] == 'sk-test-key'
# Lowercase variants should not be included
assert 'lmnr_project_api_key' not in result
assert 'Lmnr_Project_Api_Key' not in result
class TestDockerSandboxSpecEnvironmentOverride:
"""Test environment variable override integration in Docker sandbox specs."""