openhands 1.0.1__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openhands might be problematic. Click here for more details.

Files changed (59) hide show
  1. {openhands-1.0.1 → openhands-1.0.3}/PKG-INFO +5 -5
  2. {openhands-1.0.1 → openhands-1.0.3}/README.md +2 -2
  3. {openhands-1.0.1 → openhands-1.0.3}/build.py +3 -4
  4. {openhands-1.0.1 → openhands-1.0.3}/openhands.spec +2 -2
  5. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/gui_launcher.py +2 -2
  6. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/pt_style.py +1 -1
  7. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/runner.py +1 -0
  8. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/settings/settings_screen.py +3 -5
  9. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/settings/store.py +1 -1
  10. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/user_actions/agent_action.py +6 -21
  11. openhands-1.0.1/openhands_cli/llm_utils.py → openhands-1.0.3/openhands_cli/utils.py +20 -1
  12. {openhands-1.0.1 → openhands-1.0.3}/pyproject.toml +13 -6
  13. openhands-1.0.3/tests/settings/test_default_agent_security_analyzer.py +104 -0
  14. {openhands-1.0.1 → openhands-1.0.3}/tests/settings/test_settings_workflow.py +4 -4
  15. {openhands-1.0.1 → openhands-1.0.3}/tests/test_confirmation_mode.py +44 -18
  16. {openhands-1.0.1 → openhands-1.0.3}/tests/test_conversation_runner.py +8 -8
  17. {openhands-1.0.1 → openhands-1.0.3}/tests/test_directory_separation.py +1 -1
  18. {openhands-1.0.1 → openhands-1.0.3}/tests/test_gui_launcher.py +1 -1
  19. {openhands-1.0.1 → openhands-1.0.3}/tests/test_mcp_config_validation.py +1 -1
  20. {openhands-1.0.1 → openhands-1.0.3}/uv.lock +1344 -955
  21. {openhands-1.0.1 → openhands-1.0.3}/.gitignore +0 -0
  22. {openhands-1.0.1 → openhands-1.0.3}/Makefile +0 -0
  23. {openhands-1.0.1 → openhands-1.0.3}/build.sh +0 -0
  24. {openhands-1.0.1 → openhands-1.0.3}/hooks/rthook_profile_imports.py +0 -0
  25. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/__init__.py +0 -0
  26. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/agent_chat.py +0 -0
  27. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/argparsers/main_parser.py +0 -0
  28. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/argparsers/serve_parser.py +0 -0
  29. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/listeners/__init__.py +0 -0
  30. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/listeners/loading_listener.py +0 -0
  31. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/listeners/pause_listener.py +0 -0
  32. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/locations.py +0 -0
  33. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/setup.py +0 -0
  34. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/simple_main.py +0 -0
  35. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/__init__.py +0 -0
  36. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/settings/mcp_screen.py +0 -0
  37. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/status.py +0 -0
  38. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/tui.py +0 -0
  39. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/tui/utils.py +0 -0
  40. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/user_actions/__init__.py +0 -0
  41. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/user_actions/exit_session.py +0 -0
  42. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/user_actions/settings_action.py +0 -0
  43. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/user_actions/types.py +0 -0
  44. {openhands-1.0.1 → openhands-1.0.3}/openhands_cli/user_actions/utils.py +0 -0
  45. {openhands-1.0.1 → openhands-1.0.3}/tests/__init__.py +0 -0
  46. {openhands-1.0.1 → openhands-1.0.3}/tests/commands/test_confirm_command.py +0 -0
  47. {openhands-1.0.1 → openhands-1.0.3}/tests/commands/test_new_command.py +0 -0
  48. {openhands-1.0.1 → openhands-1.0.3}/tests/commands/test_status_command.py +0 -0
  49. {openhands-1.0.1 → openhands-1.0.3}/tests/conftest.py +0 -0
  50. {openhands-1.0.1 → openhands-1.0.3}/tests/settings/test_api_key_preservation.py +0 -0
  51. {openhands-1.0.1 → openhands-1.0.3}/tests/settings/test_first_time_user_settings.py +0 -0
  52. {openhands-1.0.1 → openhands-1.0.3}/tests/settings/test_settings_input.py +0 -0
  53. {openhands-1.0.1 → openhands-1.0.3}/tests/test_exit_session_confirmation.py +0 -0
  54. {openhands-1.0.1 → openhands-1.0.3}/tests/test_loading.py +0 -0
  55. {openhands-1.0.1 → openhands-1.0.3}/tests/test_main.py +0 -0
  56. {openhands-1.0.1 → openhands-1.0.3}/tests/test_pause_listener.py +0 -0
  57. {openhands-1.0.1 → openhands-1.0.3}/tests/test_session_prompter.py +0 -0
  58. {openhands-1.0.1 → openhands-1.0.3}/tests/test_tui.py +0 -0
  59. {openhands-1.0.1 → openhands-1.0.3}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: OpenHands CLI - Terminal User Interface for OpenHands AI Agent
5
5
  Author-email: OpenHands Team <contact@all-hands.dev>
6
6
  License: MIT
@@ -8,17 +8,17 @@ Classifier: Programming Language :: Python :: 3 :: Only
8
8
  Classifier: Programming Language :: Python :: 3.12
9
9
  Classifier: Programming Language :: Python :: 3.13
10
10
  Requires-Python: >=3.12
11
- Requires-Dist: openhands-sdk
12
- Requires-Dist: openhands-tools
11
+ Requires-Dist: openhands-sdk==1.0.0a5
12
+ Requires-Dist: openhands-tools==1.0.0a5
13
13
  Requires-Dist: prompt-toolkit>=3
14
14
  Requires-Dist: typer>=0.17.4
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  # OpenHands V1 CLI
18
18
 
19
- A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/All-Hands-AI/agent-sdk)).
19
+ A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/OpenHands/agent-sdk)).
20
20
 
21
- The [OpenHands V0 CLI (legacy)](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/cli) is being deprecated.
21
+ The [OpenHands V0 CLI (legacy)](https://github.com/OpenHands/OpenHands/tree/main/openhands/cli) is being deprecated.
22
22
 
23
23
  ---
24
24
 
@@ -1,8 +1,8 @@
1
1
  # OpenHands V1 CLI
2
2
 
3
- A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/All-Hands-AI/agent-sdk)).
3
+ A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/OpenHands/agent-sdk)).
4
4
 
5
- The [OpenHands V0 CLI (legacy)](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/cli) is being deprecated.
5
+ The [OpenHands V0 CLI (legacy)](https://github.com/OpenHands/OpenHands/tree/main/openhands/cli) is being deprecated.
6
6
 
7
7
  ---
8
8
 
@@ -15,13 +15,12 @@ import sys
15
15
  import time
16
16
  from pathlib import Path
17
17
 
18
- from openhands_cli.llm_utils import get_llm_metadata
19
- from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR, WORK_DIR
18
+ from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
19
+ from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
20
20
 
21
21
  from openhands.sdk import LLM
22
- from openhands.tools.preset.default import get_default_agent
23
22
 
24
- dummy_agent = get_default_agent(
23
+ dummy_agent = get_default_cli_agent(
25
24
  llm=LLM(
26
25
  model='dummy-model',
27
26
  api_key='dummy-key',
@@ -32,8 +32,8 @@ a = Analysis(
32
32
  *collect_data_files('litellm'),
33
33
  *collect_data_files('fastmcp'),
34
34
  *collect_data_files('mcp'),
35
- # Include Jinja prompt templates required by the agent SDK
36
- *collect_data_files('openhands.sdk.agent', includes=['prompts/*.j2']),
35
+ # Include all data files from openhands.sdk (templates, configs, etc.)
36
+ *collect_data_files('openhands.sdk'),
37
37
  # Include package metadata for importlib.metadata
38
38
  *copy_metadata('fastmcp'),
39
39
  ],
@@ -104,8 +104,8 @@ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
104
104
 
105
105
  # Get the current version for the Docker image
106
106
  version = get_openhands_version()
107
- runtime_image = f'docker.all-hands.dev/all-hands-ai/runtime:{version}-nikolaik'
108
- app_image = f'docker.all-hands.dev/all-hands-ai/openhands:{version}'
107
+ runtime_image = f'docker.all-hands.dev/openhands/runtime:{version}-nikolaik'
108
+ app_image = f'docker.all-hands.dev/openhands/openhands:{version}'
109
109
 
110
110
  print_formatted_text(HTML('<grey>Pulling required Docker images...</grey>'))
111
111
 
@@ -20,7 +20,7 @@ def get_cli_style() -> BaseStyle:
20
20
  'prompt': f'{COLOR_GOLD} bold',
21
21
  # Ensure good contrast for fuzzy matches on the selected completion row
22
22
  # across terminals/themes (e.g., Ubuntu GNOME, Alacritty, Kitty).
23
- # See https://github.com/All-Hands-AI/OpenHands/issues/10330
23
+ # See https://github.com/OpenHands/OpenHands/issues/10330
24
24
  'completion-menu.completion.current fuzzymatch.outside': 'fg:#ffffff bg:#888888',
25
25
  'selected': COLOR_GOLD,
26
26
  'risk-high': '#FF0000 bold', # Red bold for HIGH risk
@@ -120,6 +120,7 @@ class ConversationRunner:
120
120
  else:
121
121
  raise Exception('Infinite loop')
122
122
 
123
+
123
124
  def _handle_confirmation_request(self) -> UserConfirmation:
124
125
  """Handle confirmation request from user.
125
126
 
@@ -1,13 +1,11 @@
1
1
  import os
2
2
 
3
3
  from openhands.sdk import LLM, BaseConversation, LocalFileStore
4
- from openhands.sdk.security.confirmation_policy import NeverConfirm
5
- from openhands.tools.preset.default import get_default_agent
6
4
  from prompt_toolkit import HTML, print_formatted_text
7
5
  from prompt_toolkit.shortcuts import print_container
8
6
  from prompt_toolkit.widgets import Frame, TextArea
9
7
 
10
- from openhands_cli.llm_utils import get_llm_metadata
8
+ from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
11
9
  from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
12
10
  from openhands_cli.pt_style import COLOR_GREY
13
11
  from openhands_cli.tui.settings.store import AgentStore
@@ -176,13 +174,13 @@ class SettingsScreen:
176
174
  model=model,
177
175
  api_key=api_key,
178
176
  base_url=base_url,
179
- service_id='agent',
177
+ usage_id='agent',
180
178
  metadata=get_llm_metadata(model_name=model, llm_type='agent'),
181
179
  )
182
180
 
183
181
  agent = self.agent_store.load()
184
182
  if not agent:
185
- agent = get_default_agent(llm=llm, cli_mode=True)
183
+ agent = get_default_cli_agent(llm=llm)
186
184
 
187
185
  agent = agent.model_copy(update={'llm': llm})
188
186
  self.agent_store.save(agent)
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
  from fastmcp.mcp_config import MCPConfig
8
- from openhands_cli.llm_utils import get_llm_metadata
8
+ from openhands_cli.utils import get_llm_metadata
9
9
  from openhands_cli.locations import (
10
10
  AGENT_SETTINGS_PATH,
11
11
  MCP_CONFIG_FILE,
@@ -44,8 +44,7 @@ def ask_user_confirmation(
44
44
  question = 'Choose an option:'
45
45
  options = [
46
46
  'Yes, proceed',
47
- 'No, reject (w/o reason)',
48
- 'No, reject with reason',
47
+ 'Reject',
49
48
  "Always proceed (don't ask again)",
50
49
  ]
51
50
 
@@ -61,32 +60,18 @@ def ask_user_confirmation(
61
60
  if index == 0:
62
61
  return ConfirmationResult(decision=UserConfirmation.ACCEPT)
63
62
  elif index == 1:
64
- return ConfirmationResult(decision=UserConfirmation.REJECT)
65
- elif index == 2:
63
+ # Handle "Reject" option with optional reason
66
64
  try:
67
- reason_result = cli_text_input(
68
- 'Please enter your reason for rejecting these actions: '
69
- )
70
- except Exception:
71
- return ConfirmationResult(decision=UserConfirmation.DEFER)
72
-
73
- # Support both string return and (reason, cancelled) tuple for tests
74
- cancelled = False
75
- if isinstance(reason_result, tuple) and len(reason_result) >= 1:
76
- reason = reason_result[0] or ''
77
- cancelled = bool(reason_result[1]) if len(reason_result) > 1 else False
78
- else:
79
- reason = str(reason_result or '').strip()
80
-
81
- if cancelled:
65
+ reason = cli_text_input('Reason (and let OpenHands know why): ').strip()
66
+ except (EOFError, KeyboardInterrupt):
82
67
  return ConfirmationResult(decision=UserConfirmation.DEFER)
83
68
 
84
69
  return ConfirmationResult(decision=UserConfirmation.REJECT, reason=reason)
85
- elif index == 3:
70
+ elif index == 2:
86
71
  return ConfirmationResult(
87
72
  decision=UserConfirmation.ACCEPT, policy_change=NeverConfirm()
88
73
  )
89
- elif index == 4:
74
+ elif index == 3:
90
75
  return ConfirmationResult(
91
76
  decision=UserConfirmation.ACCEPT,
92
77
  policy_change=ConfirmRisky(threshold=SecurityRisk.HIGH),
@@ -2,7 +2,9 @@
2
2
 
3
3
  import os
4
4
  from typing import Any
5
-
5
+ from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
6
+ from openhands.tools.preset import get_default_agent
7
+ from openhands.sdk import LLM
6
8
 
7
9
  def get_llm_metadata(
8
10
  model_name: str,
@@ -55,3 +57,20 @@ def get_llm_metadata(
55
57
  if user_id is not None:
56
58
  metadata['trace_user_id'] = user_id
57
59
  return metadata
60
+
61
+
62
+ def get_default_cli_agent(
63
+ llm: LLM
64
+ ):
65
+ agent = get_default_agent(
66
+ llm=llm,
67
+ cli_mode=True
68
+ )
69
+
70
+ agent = agent.model_copy(
71
+ update={
72
+ 'security_analyzer': LLMSecurityAnalyzer()
73
+ }
74
+ )
75
+
76
+ return agent
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
4
4
 
5
5
  [project]
6
6
  name = "openhands"
7
- version = "1.0.1"
7
+ version = "1.0.3"
8
8
  description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -15,9 +15,11 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.12",
16
16
  "Programming Language :: Python :: 3.13",
17
17
  ]
18
+ # Using Git URLs for dependencies so installs from PyPI pull from GitHub
19
+ # TODO: pin package versions once agent-sdk has published PyPI packages
18
20
  dependencies = [
19
- "openhands-sdk",
20
- "openhands-tools",
21
+ "openhands-sdk==1.0.0a5",
22
+ "openhands-tools==1.0.0a5",
21
23
  "prompt-toolkit>=3",
22
24
  "typer>=0.17.4",
23
25
  ]
@@ -41,6 +43,9 @@ dev = [
41
43
  "ruff>=0.11.8",
42
44
  ]
43
45
 
46
+ [tool.hatch.metadata]
47
+ allow-direct-references = true
48
+
44
49
  [tool.hatch.build.targets.wheel]
45
50
  packages = [ "openhands_cli" ]
46
51
 
@@ -94,6 +99,8 @@ warn_unused_configs = true
94
99
  disallow_untyped_defs = true
95
100
  ignore_missing_imports = true
96
101
 
97
- [tool.uv.sources]
98
- openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "50b094a92817e448ec4352d2950df4f19edd5a9f" }
99
- openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "50b094a92817e448ec4352d2950df4f19edd5a9f" }
102
+ # UNCOMMENT TO USE EXACT COMMIT FROM AGENT-SDK
103
+
104
+ # [tool.uv.sources]
105
+ # openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-sdk", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
106
+ # openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-tools", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
@@ -0,0 +1,104 @@
1
+ """Test that first-time settings screen usage creates a default agent with security analyzer."""
2
+
3
+ from unittest.mock import patch
4
+ import pytest
5
+ from openhands_cli.tui.settings.settings_screen import SettingsScreen
6
+ from openhands_cli.user_actions.settings_action import SettingsType
7
+ from openhands.sdk import LLM
8
+ from pydantic import SecretStr
9
+
10
+
11
+ def test_first_time_settings_creates_default_agent_with_security_analyzer():
12
+ """Test that using the settings screen for the first time creates a default agent with a non-None security analyzer."""
13
+
14
+ # Create a settings screen instance (no conversation initially)
15
+ screen = SettingsScreen(conversation=None)
16
+
17
+ # Mock all the user interaction steps to simulate first-time setup
18
+ with (
19
+ patch(
20
+ 'openhands_cli.tui.settings.settings_screen.settings_type_confirmation',
21
+ return_value=SettingsType.BASIC,
22
+ ),
23
+ patch(
24
+ 'openhands_cli.tui.settings.settings_screen.choose_llm_provider',
25
+ return_value='openai',
26
+ ),
27
+ patch(
28
+ 'openhands_cli.tui.settings.settings_screen.choose_llm_model',
29
+ return_value='gpt-4o-mini',
30
+ ),
31
+ patch(
32
+ 'openhands_cli.tui.settings.settings_screen.prompt_api_key',
33
+ return_value='sk-test-key-123',
34
+ ),
35
+ patch(
36
+ 'openhands_cli.tui.settings.settings_screen.save_settings_confirmation',
37
+ return_value=True,
38
+ ),
39
+ ):
40
+ # Run the settings configuration workflow
41
+ screen.configure_settings(first_time=True)
42
+
43
+ # Load the saved agent from the store
44
+ saved_agent = screen.agent_store.load()
45
+
46
+ # Verify that an agent was created and saved
47
+ assert saved_agent is not None, "Agent should be created and saved after first-time settings configuration"
48
+
49
+ # Verify that the agent has the expected LLM configuration
50
+ assert saved_agent.llm.model == 'openai/gpt-4o-mini', f"Expected model 'openai/gpt-4o-mini', got '{saved_agent.llm.model}'"
51
+ assert saved_agent.llm.api_key.get_secret_value() == 'sk-test-key-123', "API key should match the provided value"
52
+
53
+ # Verify that the agent has a security analyzer and it's not None
54
+ assert hasattr(saved_agent, 'security_analyzer'), "Agent should have a security_analyzer attribute"
55
+ assert saved_agent.security_analyzer is not None, "Security analyzer should not be None"
56
+
57
+ # Verify the security analyzer has the expected type/kind
58
+ assert hasattr(saved_agent.security_analyzer, 'kind'), "Security analyzer should have a 'kind' attribute"
59
+ assert saved_agent.security_analyzer.kind == 'LLMSecurityAnalyzer', f"Expected security analyzer kind 'LLMSecurityAnalyzer', got '{saved_agent.security_analyzer.kind}'"
60
+
61
+
62
+ def test_first_time_settings_with_advanced_configuration():
63
+ """Test that advanced settings also create a default agent with security analyzer."""
64
+
65
+ screen = SettingsScreen(conversation=None)
66
+
67
+ with (
68
+ patch(
69
+ 'openhands_cli.tui.settings.settings_screen.settings_type_confirmation',
70
+ return_value=SettingsType.ADVANCED,
71
+ ),
72
+ patch(
73
+ 'openhands_cli.tui.settings.settings_screen.prompt_custom_model',
74
+ return_value='anthropic/claude-3-5-sonnet',
75
+ ),
76
+ patch(
77
+ 'openhands_cli.tui.settings.settings_screen.prompt_base_url',
78
+ return_value='https://api.anthropic.com',
79
+ ),
80
+ patch(
81
+ 'openhands_cli.tui.settings.settings_screen.prompt_api_key',
82
+ return_value='sk-ant-test-key',
83
+ ),
84
+ patch(
85
+ 'openhands_cli.tui.settings.settings_screen.choose_memory_condensation',
86
+ return_value=True,
87
+ ),
88
+ patch(
89
+ 'openhands_cli.tui.settings.settings_screen.save_settings_confirmation',
90
+ return_value=True,
91
+ ),
92
+ ):
93
+ screen.configure_settings(first_time=True)
94
+
95
+ saved_agent = screen.agent_store.load()
96
+
97
+ # Verify agent creation and security analyzer
98
+ assert saved_agent is not None, "Agent should be created with advanced settings"
99
+ assert saved_agent.security_analyzer is not None, "Security analyzer should not be None in advanced settings"
100
+ assert saved_agent.security_analyzer.kind == 'LLMSecurityAnalyzer', "Security analyzer should be LLMSecurityAnalyzer"
101
+
102
+ # Verify advanced settings were applied
103
+ assert saved_agent.llm.model == 'anthropic/claude-3-5-sonnet', "Custom model should be set"
104
+ assert saved_agent.llm.base_url == 'https://api.anthropic.com', "Base URL should be set"
@@ -6,10 +6,10 @@ import pytest
6
6
  from openhands_cli.tui.settings.settings_screen import SettingsScreen
7
7
  from openhands_cli.tui.settings.store import AgentStore
8
8
  from openhands_cli.user_actions.settings_action import SettingsType
9
+ from openhands_cli.utils import get_default_cli_agent
9
10
  from pydantic import SecretStr
10
11
 
11
12
  from openhands.sdk import LLM, Conversation, LocalFileStore
12
- from openhands.tools.preset.default import get_default_agent
13
13
 
14
14
 
15
15
  def read_json(path: Path) -> dict:
@@ -18,7 +18,7 @@ def read_json(path: Path) -> dict:
18
18
 
19
19
 
20
20
  def make_screen_with_conversation(model='openai/gpt-4o-mini', api_key='sk-xyz'):
21
- llm = LLM(model=model, api_key=SecretStr(api_key), service_id='test-service')
21
+ llm = LLM(model=model, api_key=SecretStr(api_key), usage_id='test-service')
22
22
  # Conversation(agent) signature may vary across versions; adapt if needed:
23
23
  from openhands.sdk.agent import Agent
24
24
 
@@ -30,8 +30,8 @@ def make_screen_with_conversation(model='openai/gpt-4o-mini', api_key='sk-xyz'):
30
30
  def seed_file(path: Path, model: str = 'openai/gpt-4o-mini', api_key: str = 'sk-old'):
31
31
  store = AgentStore()
32
32
  store.file_store = LocalFileStore(root=str(path))
33
- agent = get_default_agent(
34
- llm=LLM(model=model, api_key=SecretStr(api_key), service_id='test-service')
33
+ agent = get_default_cli_agent(
34
+ llm=LLM(model=model, api_key=SecretStr(api_key), usage_id='test-service')
35
35
  )
36
36
  store.save(agent)
37
37
 
@@ -147,10 +147,12 @@ class TestConfirmationMode:
147
147
  assert result.policy_change is None
148
148
  assert result.policy_change is None
149
149
 
150
+ @patch('openhands_cli.user_actions.agent_action.cli_text_input')
150
151
  @patch('openhands_cli.user_actions.agent_action.cli_confirm')
151
- def test_ask_user_confirmation_no(self, mock_cli_confirm: Any) -> None:
152
- """Test that ask_user_confirmation returns REJECT when user selects no."""
153
- mock_cli_confirm.return_value = 1 # Second option (No, reject)
152
+ def test_ask_user_confirmation_no(self, mock_cli_confirm: Any, mock_cli_text_input: Any) -> None:
153
+ """Test that ask_user_confirmation returns REJECT when user selects reject without reason."""
154
+ mock_cli_confirm.return_value = 1 # Second option (Reject)
155
+ mock_cli_text_input.return_value = '' # Empty reason (reject without reason)
154
156
 
155
157
  mock_action = MagicMock()
156
158
  mock_action.tool_name = 'bash'
@@ -163,6 +165,7 @@ class TestConfirmationMode:
163
165
  assert result.reason == ''
164
166
  assert result.policy_change is None
165
167
  assert result.policy_change is None
168
+ mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
166
169
 
167
170
  @patch('openhands_cli.user_actions.agent_action.cli_confirm')
168
171
  def test_ask_user_confirmation_y_shorthand(self, mock_cli_confirm: Any) -> None:
@@ -179,10 +182,12 @@ class TestConfirmationMode:
179
182
  assert result.reason == ''
180
183
  assert result.policy_change is None
181
184
 
185
+ @patch('openhands_cli.user_actions.agent_action.cli_text_input')
182
186
  @patch('openhands_cli.user_actions.agent_action.cli_confirm')
183
- def test_ask_user_confirmation_n_shorthand(self, mock_cli_confirm: Any) -> None:
184
- """Test that ask_user_confirmation accepts second option as no."""
185
- mock_cli_confirm.return_value = 1 # Second option (No, reject)
187
+ def test_ask_user_confirmation_n_shorthand(self, mock_cli_confirm: Any, mock_cli_text_input: Any) -> None:
188
+ """Test that ask_user_confirmation accepts second option as reject."""
189
+ mock_cli_confirm.return_value = 1 # Second option (Reject)
190
+ mock_cli_text_input.return_value = '' # Empty reason (reject without reason)
186
191
 
187
192
  mock_action = MagicMock()
188
193
  mock_action.tool_name = 'bash'
@@ -193,6 +198,7 @@ class TestConfirmationMode:
193
198
  assert isinstance(result, ConfirmationResult)
194
199
  assert result.reason == ''
195
200
  assert result.policy_change is None
201
+ mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
196
202
 
197
203
  @patch('openhands_cli.user_actions.agent_action.cli_confirm')
198
204
  def test_ask_user_confirmation_invalid_then_yes(
@@ -278,9 +284,9 @@ class TestConfirmationMode:
278
284
  def test_ask_user_confirmation_no_with_reason(
279
285
  self, mock_cli_confirm: Any, mock_cli_text_input: Any
280
286
  ) -> None:
281
- """Test that ask_user_confirmation returns REJECT when user selects 'No (with reason)'."""
282
- mock_cli_confirm.return_value = 2 # Third option (No, with reason)
283
- mock_cli_text_input.return_value = ('This action is too risky', False)
287
+ """Test that ask_user_confirmation returns REJECT when user selects 'Reject' and provides a reason."""
288
+ mock_cli_confirm.return_value = 1 # Second option (Reject)
289
+ mock_cli_text_input.return_value = 'This action is too risky'
284
290
 
285
291
  mock_action = MagicMock()
286
292
  mock_action.tool_name = 'bash'
@@ -291,7 +297,7 @@ class TestConfirmationMode:
291
297
  assert result.decision == UserConfirmation.REJECT
292
298
  assert result.reason == 'This action is too risky'
293
299
  assert result.policy_change is None
294
- mock_cli_text_input.assert_called_once()
300
+ mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
295
301
 
296
302
  @patch('openhands_cli.user_actions.agent_action.cli_text_input')
297
303
  @patch('openhands_cli.user_actions.agent_action.cli_confirm')
@@ -299,8 +305,8 @@ class TestConfirmationMode:
299
305
  self, mock_cli_confirm: Any, mock_cli_text_input: Any
300
306
  ) -> None:
301
307
  """Test that ask_user_confirmation falls back to DEFER when reason input is cancelled."""
302
- mock_cli_confirm.return_value = 2 # Third option (No, with reason)
303
- mock_cli_text_input.return_value = ('', True) # User cancelled reason input
308
+ mock_cli_confirm.return_value = 1 # Second option (Reject)
309
+ mock_cli_text_input.side_effect = KeyboardInterrupt() # User cancelled reason input
304
310
 
305
311
  mock_action = MagicMock()
306
312
  mock_action.tool_name = 'bash'
@@ -311,7 +317,27 @@ class TestConfirmationMode:
311
317
  assert isinstance(result, ConfirmationResult)
312
318
  assert result.reason == ''
313
319
  assert result.policy_change is None
314
- mock_cli_text_input.assert_called_once()
320
+ mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
321
+
322
+ @patch('openhands_cli.user_actions.agent_action.cli_text_input')
323
+ @patch('openhands_cli.user_actions.agent_action.cli_confirm')
324
+ def test_ask_user_confirmation_reject_empty_reason(
325
+ self, mock_cli_confirm: Any, mock_cli_text_input: Any
326
+ ) -> None:
327
+ """Test that ask_user_confirmation handles empty reason input correctly."""
328
+ mock_cli_confirm.return_value = 1 # Second option (Reject)
329
+ mock_cli_text_input.return_value = ' ' # Whitespace-only reason (should be treated as empty)
330
+
331
+ mock_action = MagicMock()
332
+ mock_action.tool_name = 'bash'
333
+ mock_action.action = 'dangerous command'
334
+
335
+ result = ask_user_confirmation([mock_action])
336
+ assert result.decision == UserConfirmation.REJECT
337
+ assert isinstance(result, ConfirmationResult)
338
+ assert result.reason == '' # Should be empty after stripping whitespace
339
+ assert result.policy_change is None
340
+ mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
315
341
 
316
342
  def test_user_confirmation_is_escapable_e2e(
317
343
  self, monkeypatch: pytest.MonkeyPatch
@@ -358,8 +384,8 @@ class TestConfirmationMode:
358
384
 
359
385
  @patch('openhands_cli.user_actions.agent_action.cli_confirm')
360
386
  def test_ask_user_confirmation_always_accept(self, mock_cli_confirm: Any) -> None:
361
- """Test that ask_user_confirmation returns ACCEPT with NeverConfirm policy when user selects fourth option."""
362
- mock_cli_confirm.return_value = 3 # Fourth option (Always proceed)
387
+ """Test that ask_user_confirmation returns ACCEPT with NeverConfirm policy when user selects third option."""
388
+ mock_cli_confirm.return_value = 2 # Third option (Always proceed)
363
389
 
364
390
  mock_action = MagicMock()
365
391
  mock_action.tool_name = 'bash'
@@ -408,7 +434,7 @@ class TestConfirmationMode:
408
434
  new_mock_conversation.id = mock_conversation.id
409
435
  new_mock_conversation.is_confirmation_mode_active = False
410
436
  mock_setup.return_value = new_mock_conversation
411
-
437
+
412
438
  result = runner._handle_confirmation_request()
413
439
 
414
440
  # Verify that confirmation mode was disabled
@@ -426,9 +452,9 @@ class TestConfirmationMode:
426
452
  def test_ask_user_confirmation_auto_confirm_safe(
427
453
  self, mock_cli_confirm: Any
428
454
  ) -> None:
429
- """Test that ask_user_confirmation returns ACCEPT with policy_change when user selects fifth option."""
455
+ """Test that ask_user_confirmation returns ACCEPT with policy_change when user selects fourth option."""
430
456
  mock_cli_confirm.return_value = (
431
- 4 # Fifth option (Auto-confirm LOW/MEDIUM, ask for HIGH)
457
+ 3 # Fourth option (Auto-confirm LOW/MEDIUM, ask for HIGH)
432
458
  )
433
459
 
434
460
  mock_action = MagicMock()
@@ -6,13 +6,13 @@ from openhands_cli.runner import ConversationRunner
6
6
  from openhands_cli.user_actions.types import UserConfirmation
7
7
  from pydantic import ConfigDict, SecretStr, model_validator
8
8
 
9
- from openhands.sdk import Conversation, ConversationCallbackType
9
+ from openhands.sdk import Conversation, ConversationCallbackType, LocalConversation
10
10
  from openhands.sdk.agent.base import AgentBase
11
11
  from openhands.sdk.conversation import ConversationState
12
12
  from openhands.sdk.conversation.state import AgentExecutionStatus
13
13
  from openhands.sdk.llm import LLM
14
14
  from openhands.sdk.security.confirmation_policy import AlwaysConfirm, NeverConfirm
15
-
15
+ from unittest.mock import MagicMock
16
16
 
17
17
  class FakeLLM(LLM):
18
18
  @model_validator(mode='after')
@@ -41,16 +41,16 @@ class FakeAgent(AgentBase):
41
41
  pass
42
42
 
43
43
  def step(
44
- self, state: ConversationState, on_event: ConversationCallbackType
44
+ self, conversation: LocalConversation, on_event: ConversationCallbackType
45
45
  ) -> None:
46
46
  self.step_count += 1
47
47
  if self.step_count == self.finish_on_step:
48
- state.agent_status = AgentExecutionStatus.FINISHED
48
+ conversation.state.agent_status = AgentExecutionStatus.FINISHED
49
49
 
50
50
 
51
51
  @pytest.fixture()
52
52
  def agent() -> FakeAgent:
53
- llm = LLM(**default_config(), service_id='test-service')
53
+ llm = LLM(**default_config(), usage_id='test-service')
54
54
  return FakeAgent(llm=llm, tools=[])
55
55
 
56
56
 
@@ -102,15 +102,15 @@ class TestConversationRunner:
102
102
  """
103
103
  if final_status == AgentExecutionStatus.FINISHED:
104
104
  agent.finish_on_step = 1
105
-
105
+
106
106
  # Add a mock security analyzer to enable confirmation mode
107
- from unittest.mock import MagicMock
108
107
  agent.security_analyzer = MagicMock()
109
-
108
+
110
109
  convo = Conversation(agent)
111
110
  convo.state.agent_status = AgentExecutionStatus.WAITING_FOR_CONFIRMATION
112
111
  cr = ConversationRunner(convo)
113
112
  cr.set_confirmation_policy(AlwaysConfirm())
113
+
114
114
  with patch.object(
115
115
  cr, '_handle_confirmation_request', return_value=confirmation
116
116
  ) as mock_confirmation_request:
@@ -37,7 +37,7 @@ class TestToolFix:
37
37
  """Test that entire tools list is replaced with default tools when loading agent."""
38
38
  # Create a mock agent with different tools and working directories
39
39
  mock_agent = Agent(
40
- llm=LLM(model='test/model', api_key='test-key', service_id='test-service'),
40
+ llm=LLM(model='test/model', api_key='test-key', usage_id='test-service'),
41
41
  tools=[
42
42
  Tool(name='BashTool'),
43
43
  Tool(name='FileEditorTool'),
@@ -182,7 +182,7 @@ class TestLaunchGuiServer:
182
182
  # Check pull command
183
183
  pull_call = mock_run.call_args_list[0]
184
184
  pull_cmd = pull_call[0][0]
185
- assert pull_cmd[0:3] == ['docker', 'pull', 'docker.all-hands.dev/all-hands-ai/runtime:latest-nikolaik']
185
+ assert pull_cmd[0:3] == ['docker', 'pull', 'docker.all-hands.dev/openhands/runtime:latest-nikolaik']
186
186
 
187
187
  # Check run command
188
188
  run_call = mock_run.call_args_list[1]
@@ -26,7 +26,7 @@ def _create_agent(mcp_config=None) -> Agent:
26
26
  if mcp_config is None:
27
27
  mcp_config = {}
28
28
  return Agent(
29
- llm=LLM(model='test-model', api_key='test-key', service_id='test-service'),
29
+ llm=LLM(model='test-model', api_key='test-key', usage_id='test-service'),
30
30
  tools=[],
31
31
  mcp_config=mcp_config,
32
32
  )