openhands-tools 1.29.3__tar.gz → 1.30.0__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.
Files changed (101) hide show
  1. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/PKG-INFO +1 -1
  2. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/editor.py +4 -2
  3. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/__init__.py +6 -1
  4. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/default.py +32 -12
  5. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/task/manager.py +1 -0
  6. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/definition.py +7 -1
  7. openhands_tools-1.30.0/openhands/tools/terminal/env.py +39 -0
  8. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/impl.py +12 -6
  9. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/factory.py +17 -5
  10. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +8 -2
  11. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +13 -4
  12. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/tmux_terminal.py +8 -2
  13. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/windows_terminal.py +8 -2
  14. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands_tools.egg-info/PKG-INFO +1 -1
  15. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands_tools.egg-info/SOURCES.txt +2 -0
  16. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/pyproject.toml +1 -1
  17. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/__init__.py +0 -0
  18. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/apply_patch/__init__.py +0 -0
  19. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/apply_patch/core.py +0 -0
  20. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/apply_patch/definition.py +0 -0
  21. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/__init__.py +0 -0
  22. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/definition.py +0 -0
  23. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/event_storage.py +0 -0
  24. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/impl.py +0 -0
  25. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  26. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/recording.py +0 -0
  27. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/browser_use/server.py +0 -0
  28. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/delegate/__init__.py +0 -0
  29. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/delegate/definition.py +0 -0
  30. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/delegate/impl.py +0 -0
  31. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/delegate/visualizer.py +0 -0
  32. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/__init__.py +0 -0
  33. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/definition.py +0 -0
  34. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/exceptions.py +0 -0
  35. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/impl.py +0 -0
  36. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  37. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/config.py +0 -0
  38. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  39. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  40. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  41. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  42. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/history.py +0 -0
  43. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  44. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/__init__.py +0 -0
  45. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  46. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/edit/definition.py +0 -0
  47. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/edit/impl.py +0 -0
  48. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  49. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  50. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  51. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  52. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  53. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  54. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  55. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  56. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  57. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/glob/__init__.py +0 -0
  58. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/glob/definition.py +0 -0
  59. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/glob/impl.py +0 -0
  60. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/grep/__init__.py +0 -0
  61. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/grep/definition.py +0 -0
  62. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/grep/impl.py +0 -0
  63. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  64. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  65. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  66. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/gemini.py +0 -0
  67. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/gpt5.py +0 -0
  68. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/planning.py +0 -0
  69. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  70. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  71. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/default.md +0 -0
  72. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  73. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/py.typed +0 -0
  74. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/task/__init__.py +0 -0
  75. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/task/definition.py +0 -0
  76. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/task/impl.py +0 -0
  77. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/task_tracker/__init__.py +0 -0
  78. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/task_tracker/definition.py +0 -0
  79. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/__init__.py +0 -0
  80. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/constants.py +0 -0
  81. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/descriptions.py +0 -0
  82. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/metadata.py +0 -0
  83. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  84. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  85. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  86. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  87. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  88. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/command.py +0 -0
  89. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  90. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/tom_consult/__init__.py +0 -0
  91. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/tom_consult/definition.py +0 -0
  92. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/tom_consult/executor.py +0 -0
  93. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/utils/__init__.py +0 -0
  94. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/utils/timeout.py +0 -0
  95. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/workflow/__init__.py +0 -0
  96. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/workflow/definition.py +0 -0
  97. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands/tools/workflow/impl.py +0 -0
  98. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  99. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands_tools.egg-info/requires.txt +0 -0
  100. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/openhands_tools.egg-info/top_level.txt +0 -0
  101. {openhands_tools-1.29.3 → openhands_tools-1.30.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.29.3
3
+ Version: 1.30.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -214,9 +214,11 @@ class FileEditor:
214
214
  if not occurrences:
215
215
  # We found no occurrences, possibly because of extra white spaces at
216
216
  # either the front or back of the string.
217
- # Remove the white spaces and try again.
217
+ # Strip old_str to retry the *match* only. Do NOT strip new_str: it
218
+ # is the replacement content, and stripping it would silently drop
219
+ # meaningful leading/trailing whitespace (e.g. a Markdown hard line
220
+ # break or intentional indentation) the caller asked to write.
218
221
  old_str = old_str.strip()
219
- new_str = new_str.strip()
220
222
  pattern = re.escape(old_str)
221
223
  occurrences = [
222
224
  (
@@ -18,13 +18,18 @@ Notes:
18
18
  setups.
19
19
  """
20
20
 
21
- from .default import get_default_agent, register_builtins_agents
21
+ from .default import (
22
+ discover_builtin_agents,
23
+ get_default_agent,
24
+ register_builtins_agents,
25
+ )
22
26
  from .gemini import get_gemini_agent, get_gemini_tools
23
27
  from .gpt5 import get_gpt5_agent
24
28
  from .planning import get_planning_agent
25
29
 
26
30
 
27
31
  __all__ = [
32
+ "discover_builtin_agents",
28
33
  "get_default_agent",
29
34
  "get_gemini_agent",
30
35
  "get_gemini_tools",
@@ -7,7 +7,7 @@ from openhands.sdk.context.condenser import default_condenser
7
7
  from openhands.sdk.context.condenser.base import CondenserBase
8
8
  from openhands.sdk.llm.llm import LLM
9
9
  from openhands.sdk.logger import get_logger
10
- from openhands.sdk.subagent import register_agent_if_absent
10
+ from openhands.sdk.subagent import AgentDefinition, register_agent_if_absent
11
11
  from openhands.sdk.tool import Tool
12
12
 
13
13
 
@@ -89,20 +89,18 @@ def get_default_agent(
89
89
  return agent
90
90
 
91
91
 
92
- def register_builtins_agents(enable_browser: bool = True) -> list[str]:
93
- """Load and register builtin agents from ``subagent/*.md``.
94
- They are registered via `register_agent_if_absent` and will not
95
- overwrite agents already registered by programmatic calls, plugins,
96
- or project/user-level file-based definitions.
92
+ def discover_builtin_agents(enable_browser: bool = True) -> list[AgentDefinition]:
93
+ """Load builtin agent definitions (``level='builtin'``) without registering them.
94
+
95
+ Non-mutating counterpart to ``register_builtins_agents``. Browser-only agents
96
+ are skipped when ``enable_browser`` is False.
97
+
97
98
  Args:
98
- enable_browser: Whether browser tools are available. When False,
99
- agents that require browser tools (e.g. web researcher) are
100
- skipped.
99
+ enable_browser: When False, skip agents needing browser tools (web researcher).
100
+
101
101
  Returns:
102
- List of agents which were actually registered.
102
+ Builtin agent definitions with ``level="builtin"``.
103
103
  """
104
- register_default_tools(enable_browser=enable_browser)
105
-
106
104
  subagent_dir = Path(__file__).parent / "subagents"
107
105
  builtins_agents_def = load_agents_from_dir(subagent_dir)
108
106
 
@@ -115,6 +113,28 @@ def register_builtins_agents(enable_browser: bool = True) -> list[str]:
115
113
  if agent.name not in _browser_only_agents
116
114
  ]
117
115
 
116
+ return [
117
+ agent_def.model_copy(update={"level": "builtin"})
118
+ for agent_def in builtins_agents_def
119
+ ]
120
+
121
+
122
+ def register_builtins_agents(enable_browser: bool = True) -> list[str]:
123
+ """Load and register builtin agents from ``subagent/*.md``.
124
+ They are registered via `register_agent_if_absent` and will not
125
+ overwrite agents already registered by programmatic calls, plugins,
126
+ or project/user-level file-based definitions.
127
+ Args:
128
+ enable_browser: Whether browser tools are available. When False,
129
+ agents that require browser tools (e.g. web researcher) are
130
+ skipped.
131
+ Returns:
132
+ List of agents which were actually registered.
133
+ """
134
+ register_default_tools(enable_browser=enable_browser)
135
+
136
+ builtins_agents_def = discover_builtin_agents(enable_browser=enable_browser)
137
+
118
138
  registered: list[str] = []
119
139
  for agent_def in builtins_agents_def:
120
140
  factory = agent_definition_to_factory(agent_def)
@@ -312,6 +312,7 @@ class TaskManager:
312
312
  max_budget_per_run=max_budget_per_run,
313
313
  hook_config=hook_config,
314
314
  delete_on_close=True,
315
+ prompt_cache_key=str(parent.state.id),
315
316
  )
316
317
 
317
318
  def _get_sub_agent(self, subagent_type: str) -> Agent:
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  import platform
5
- from collections.abc import Sequence
5
+ from collections.abc import Mapping, Sequence
6
6
  from typing import TYPE_CHECKING, Literal
7
7
 
8
8
  from pydantic import Field
@@ -288,6 +288,8 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
288
288
  terminal_type: Literal["tmux", "subprocess", "powershell"] | None = None,
289
289
  shell_path: str | None = None,
290
290
  executor: ToolExecutor | None = None,
291
+ *,
292
+ env: Mapping[str, str] | None = None,
291
293
  ) -> Sequence["TerminalTool"]:
292
294
  """Initialize TerminalTool with executor parameters.
293
295
 
@@ -305,6 +307,9 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
305
307
  shell_path: Path to the shell binary. On Unix this applies to the
306
308
  subprocess backend; on Windows it can point to a
307
309
  PowerShell executable.
310
+ env: Extra environment variables to add to the terminal session.
311
+ These are client-controlled session settings and are not part
312
+ of the LLM-facing TerminalAction schema.
308
313
  """
309
314
  # Import here to avoid circular imports
310
315
  from openhands.tools.terminal.impl import TerminalExecutor
@@ -321,6 +326,7 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
321
326
  no_change_timeout_seconds=no_change_timeout_seconds,
322
327
  terminal_type=terminal_type,
323
328
  shell_path=shell_path,
329
+ env=env,
324
330
  full_output_save_dir=conv_state.env_observation_persistence_dir,
325
331
  )
326
332
 
@@ -0,0 +1,39 @@
1
+ """Environment helpers for terminal backends."""
2
+
3
+ import re
4
+ from collections.abc import Mapping
5
+
6
+ from openhands.sdk.utils import sanitized_env
7
+
8
+
9
+ ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
10
+
11
+
12
+ def normalize_terminal_env(
13
+ extra_env: Mapping[str, str] | None,
14
+ ) -> dict[str, str] | None:
15
+ """Validate and copy client-provided terminal environment variables."""
16
+ if extra_env is None:
17
+ return None
18
+
19
+ normalized: dict[str, str] = {}
20
+ for key, value in extra_env.items():
21
+ if not isinstance(key, str) or not ENV_VAR_NAME_RE.match(key):
22
+ raise ValueError(f"Invalid terminal environment variable name: {key!r}")
23
+ if not isinstance(value, str):
24
+ raise TypeError(
25
+ "Terminal environment variable values must be strings: "
26
+ f"{key!r}={value!r}"
27
+ )
28
+ normalized[key] = value
29
+ return normalized
30
+
31
+
32
+ def build_terminal_env(extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
33
+ """Return the sanitized process environment plus client-provided overrides."""
34
+ env = sanitized_env()
35
+ normalized = normalize_terminal_env(extra_env)
36
+ if normalized:
37
+ env.update(normalized)
38
+ env = sanitized_env(env)
39
+ return env
@@ -1,6 +1,6 @@
1
- import re
2
1
  import threading
3
2
  import time
3
+ from collections.abc import Mapping
4
4
  from contextlib import suppress
5
5
  from typing import TYPE_CHECKING, Literal
6
6
 
@@ -20,6 +20,10 @@ from openhands.tools.terminal.definition import (
20
20
  TerminalObservation,
21
21
  looks_like_python_literal_argument,
22
22
  )
23
+ from openhands.tools.terminal.env import (
24
+ ENV_VAR_NAME_RE,
25
+ normalize_terminal_env,
26
+ )
23
27
  from openhands.tools.terminal.terminal.factory import (
24
28
  _is_tmux_available,
25
29
  create_terminal_session,
@@ -55,10 +59,6 @@ _TMUX_RECOVERABLE_ERROR_MARKERS = (
55
59
 
56
60
  logger = get_logger(__name__)
57
61
 
58
- # Environment variable names must be alphanumeric + underscores, starting with
59
- # a letter or underscore. This guards against shell injection via key names.
60
- _ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
61
-
62
62
 
63
63
  class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
64
64
  shell_path: str | None
@@ -70,6 +70,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
70
70
  no_change_timeout_seconds: int | None = None,
71
71
  terminal_type: Literal["tmux", "subprocess", "powershell"] | None = None,
72
72
  shell_path: str | None = None,
73
+ env: Mapping[str, str] | None = None,
73
74
  full_output_save_dir: str | None = None,
74
75
  max_panes: int = DEFAULT_MAX_PANES,
75
76
  ):
@@ -85,6 +86,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
85
86
  shell_path: Path to the shell binary. On Unix this applies to the
86
87
  subprocess backend; on Windows it can point to a
87
88
  PowerShell executable.
89
+ env: Extra environment variables to add to the terminal session.
88
90
  full_output_save_dir: Path to directory to save full output
89
91
  logs and files, used when truncation is needed.
90
92
  max_panes: Maximum number of concurrent panes in pool mode.
@@ -94,6 +96,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
94
96
  self._username = username
95
97
  self._no_change_timeout_seconds = no_change_timeout_seconds
96
98
  self._terminal_type = terminal_type
99
+ self._env = normalize_terminal_env(env)
97
100
  self._max_panes = max_panes
98
101
  self.full_output_save_dir: str | None = full_output_save_dir
99
102
 
@@ -115,6 +118,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
115
118
  no_change_timeout_seconds=no_change_timeout_seconds,
116
119
  terminal_type=terminal_type,
117
120
  shell_path=shell_path,
121
+ env=self._env,
118
122
  )
119
123
  self._session.initialize()
120
124
  logger.info(
@@ -134,6 +138,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
134
138
  self._pool = TmuxPanePool(
135
139
  self._working_dir,
136
140
  self._username,
141
+ env=self._env,
137
142
  max_panes=self._max_panes,
138
143
  )
139
144
  self._pool.initialize()
@@ -288,7 +293,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
288
293
  ) -> str:
289
294
  valid: dict[str, str] = {}
290
295
  for key, value in env_vars.items():
291
- if _ENV_VAR_NAME_RE.match(key):
296
+ if ENV_VAR_NAME_RE.match(key):
292
297
  valid[key] = value
293
298
  else:
294
299
  logger.warning("Skipping secret with invalid env var name: %r", key)
@@ -400,6 +405,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
400
405
  no_change_timeout_seconds=original_no_change_timeout,
401
406
  terminal_type=None,
402
407
  shell_path=self.shell_path,
408
+ env=self._env,
403
409
  )
404
410
  self._session.initialize()
405
411
 
@@ -3,6 +3,7 @@
3
3
  import platform
4
4
  import subprocess
5
5
  import warnings
6
+ from collections.abc import Mapping
6
7
  from typing import Literal
7
8
 
8
9
  from openhands.sdk.logger import get_logger
@@ -64,6 +65,7 @@ def _create_windows_terminal(
64
65
  username: str | None,
65
66
  no_change_timeout_seconds: int | None,
66
67
  shell_path: str | None,
68
+ env: Mapping[str, str] | None,
67
69
  ) -> TerminalSession:
68
70
  from openhands.tools.terminal.terminal.windows_terminal import WindowsTerminal
69
71
 
@@ -71,7 +73,12 @@ def _create_windows_terminal(
71
73
  if resolved_shell_path is None:
72
74
  raise RuntimeError("PowerShell is not available on this system")
73
75
 
74
- terminal = WindowsTerminal(work_dir, username, shell_path=resolved_shell_path)
76
+ terminal = WindowsTerminal(
77
+ work_dir,
78
+ username,
79
+ shell_path=resolved_shell_path,
80
+ env=env,
81
+ )
75
82
  return TerminalSession(terminal, no_change_timeout_seconds)
76
83
 
77
84
 
@@ -81,6 +88,7 @@ def create_terminal_session(
81
88
  no_change_timeout_seconds: int | None = None,
82
89
  terminal_type: Literal["tmux", "subprocess", "powershell"] | None = None,
83
90
  shell_path: str | None = None,
91
+ env: Mapping[str, str] | None = None,
84
92
  ) -> TerminalSession:
85
93
  """Create an appropriate terminal session based on system capabilities.
86
94
 
@@ -92,6 +100,7 @@ def create_terminal_session(
92
100
  or 'powershell'). If None, auto-detect based on system capabilities.
93
101
  shell_path: Path to the shell binary. On Unix this is used for the
94
102
  subprocess backend; on Windows it can point to a PowerShell binary.
103
+ env: Extra environment variables to add to the terminal session.
95
104
 
96
105
  Returns:
97
106
  TerminalSession instance
@@ -106,7 +115,7 @@ def create_terminal_session(
106
115
  from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
107
116
 
108
117
  logger.info("Using forced TmuxTerminal")
109
- terminal = TmuxTerminal(work_dir, username)
118
+ terminal = TmuxTerminal(work_dir, username, env=env)
110
119
  return TerminalSession(terminal, no_change_timeout_seconds)
111
120
 
112
121
  if terminal_type == "powershell":
@@ -116,6 +125,7 @@ def create_terminal_session(
116
125
  username,
117
126
  no_change_timeout_seconds,
118
127
  shell_path,
128
+ env,
119
129
  )
120
130
 
121
131
  if terminal_type == "subprocess":
@@ -130,13 +140,14 @@ def create_terminal_session(
130
140
  username,
131
141
  no_change_timeout_seconds,
132
142
  shell_path,
143
+ env,
133
144
  )
134
145
  from openhands.tools.terminal.terminal.subprocess_terminal import (
135
146
  SubprocessTerminal,
136
147
  )
137
148
 
138
149
  logger.info("Using forced SubprocessTerminal")
139
- terminal = SubprocessTerminal(work_dir, username, shell_path)
150
+ terminal = SubprocessTerminal(work_dir, username, shell_path, env=env)
140
151
  return TerminalSession(terminal, no_change_timeout_seconds)
141
152
 
142
153
  raise ValueError(f"Unknown session type: {terminal_type}")
@@ -148,13 +159,14 @@ def create_terminal_session(
148
159
  username,
149
160
  no_change_timeout_seconds,
150
161
  shell_path,
162
+ env,
151
163
  )
152
164
 
153
165
  if _is_tmux_available():
154
166
  from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
155
167
 
156
168
  logger.info("Auto-detected: Using TmuxTerminal (tmux available)")
157
- terminal = TmuxTerminal(work_dir, username)
169
+ terminal = TmuxTerminal(work_dir, username, env=env)
158
170
  return TerminalSession(terminal, no_change_timeout_seconds)
159
171
 
160
172
  from openhands.tools.terminal.terminal.subprocess_terminal import (
@@ -168,5 +180,5 @@ def create_terminal_session(
168
180
  )
169
181
  logger.warning(_tmux_warning)
170
182
  warnings.warn(_tmux_warning, stacklevel=2)
171
- terminal = SubprocessTerminal(work_dir, username, shell_path)
183
+ terminal = SubprocessTerminal(work_dir, username, shell_path, env=env)
172
184
  return TerminalSession(terminal, no_change_timeout_seconds)
@@ -9,6 +9,7 @@ import subprocess
9
9
  import threading
10
10
  import time
11
11
  from collections import deque
12
+ from collections.abc import Mapping
12
13
 
13
14
 
14
15
  if platform.system() == "Windows":
@@ -22,12 +23,15 @@ import pty
22
23
  import select
23
24
 
24
25
  from openhands.sdk.logger import get_logger
25
- from openhands.sdk.utils import sanitized_env
26
26
  from openhands.tools.terminal.constants import (
27
27
  CMD_OUTPUT_PS1_BEGIN,
28
28
  CMD_OUTPUT_PS1_END,
29
29
  HISTORY_LIMIT,
30
30
  )
31
+ from openhands.tools.terminal.env import (
32
+ build_terminal_env,
33
+ normalize_terminal_env,
34
+ )
31
35
  from openhands.tools.terminal.metadata import CmdOutputMetadata
32
36
  from openhands.tools.terminal.terminal import TerminalInterface
33
37
  from openhands.tools.terminal.terminal.interface import parse_ctrl_key
@@ -84,6 +88,7 @@ class SubprocessTerminal(TerminalInterface):
84
88
  work_dir: str,
85
89
  username: str | None = None,
86
90
  shell_path: str | None = None,
91
+ env: Mapping[str, str] | None = None,
87
92
  ):
88
93
  super().__init__(work_dir, username)
89
94
  self.PS1 = CmdOutputMetadata.to_ps1_prompt()
@@ -96,6 +101,7 @@ class SubprocessTerminal(TerminalInterface):
96
101
  self.reader_thread = None
97
102
  self._current_command_running = False
98
103
  self.shell_path = shell_path
104
+ self._env = normalize_terminal_env(env)
99
105
 
100
106
  # ------------------------- Lifecycle -------------------------
101
107
 
@@ -136,7 +142,7 @@ class SubprocessTerminal(TerminalInterface):
136
142
  logger.info(f"Using shell: {resolved_shell_path}")
137
143
 
138
144
  # Inherit environment variables from the parent process
139
- env = sanitized_env()
145
+ env = build_terminal_env(self._env)
140
146
  # Disable interactive pagers (git, man, systemctl, ...) so commands that
141
147
  # auto-launch `less` on a TTY don't capture the PTY and wedge the session.
142
148
  env.setdefault("GIT_PAGER", "cat")
@@ -10,7 +10,7 @@ import threading
10
10
  import time
11
11
  import uuid
12
12
  from collections import deque
13
- from collections.abc import Iterator
13
+ from collections.abc import Iterator, Mapping
14
14
  from contextlib import contextmanager, suppress
15
15
  from dataclasses import dataclass, field
16
16
  from typing import Final
@@ -18,13 +18,16 @@ from typing import Final
18
18
  import libtmux
19
19
 
20
20
  from openhands.sdk.logger import get_logger
21
- from openhands.sdk.utils import sanitized_env
22
21
  from openhands.tools.terminal.constants import (
23
22
  HISTORY_LIMIT,
24
23
  TMUX_SESSION_HEIGHT,
25
24
  TMUX_SESSION_WIDTH,
26
25
  TMUX_SOCKET_NAME,
27
26
  )
27
+ from openhands.tools.terminal.env import (
28
+ build_terminal_env,
29
+ normalize_terminal_env,
30
+ )
28
31
  from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
29
32
 
30
33
 
@@ -80,6 +83,7 @@ class TmuxPanePool:
80
83
 
81
84
  work_dir: str
82
85
  username: str | None = None
86
+ env: Mapping[str, str] | None = None
83
87
  max_panes: int = DEFAULT_MAX_PANES
84
88
 
85
89
  # tmux handles
@@ -105,6 +109,7 @@ class TmuxPanePool:
105
109
  def __post_init__(self) -> None:
106
110
  if self.max_panes < 1:
107
111
  raise ValueError(f"max_panes must be >= 1, but got {self.max_panes}.")
112
+ self.env = normalize_terminal_env(self.env)
108
113
  self._semaphore = threading.Semaphore(self.max_panes)
109
114
 
110
115
  def initialize(self) -> None:
@@ -112,7 +117,7 @@ class TmuxPanePool:
112
117
  if self._initialized:
113
118
  return
114
119
 
115
- env = sanitized_env()
120
+ env = build_terminal_env(self.env)
116
121
  self._server = libtmux.Server(socket_name=TMUX_SOCKET_NAME, environment=env)
117
122
  session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
118
123
  self._session = self._server.new_session(
@@ -182,7 +187,11 @@ class TmuxPanePool:
182
187
 
183
188
  # Use PooledTmuxTerminal which overrides close() to only kill
184
189
  # this terminal's window instead of the entire shared tmux session.
185
- terminal = PooledTmuxTerminal(work_dir=self.work_dir, username=self.username)
190
+ terminal = PooledTmuxTerminal(
191
+ work_dir=self.work_dir,
192
+ username=self.username,
193
+ env=self.env,
194
+ )
186
195
  terminal.server = self._server # type: ignore[assignment]
187
196
  terminal.session = self._session
188
197
  terminal.window = window
@@ -2,17 +2,21 @@
2
2
 
3
3
  import time
4
4
  import uuid
5
+ from collections.abc import Mapping
5
6
 
6
7
  import libtmux
7
8
 
8
9
  from openhands.sdk.logger import get_logger
9
- from openhands.sdk.utils import sanitized_env
10
10
  from openhands.tools.terminal.constants import (
11
11
  HISTORY_LIMIT,
12
12
  TMUX_SESSION_HEIGHT,
13
13
  TMUX_SESSION_WIDTH,
14
14
  TMUX_SOCKET_NAME,
15
15
  )
16
+ from openhands.tools.terminal.env import (
17
+ build_terminal_env,
18
+ normalize_terminal_env,
19
+ )
16
20
  from openhands.tools.terminal.metadata import CmdOutputMetadata
17
21
  from openhands.tools.terminal.terminal import TerminalInterface
18
22
  from openhands.tools.terminal.terminal.interface import parse_ctrl_key
@@ -57,16 +61,18 @@ class TmuxTerminal(TerminalInterface):
57
61
  self,
58
62
  work_dir: str,
59
63
  username: str | None = None,
64
+ env: Mapping[str, str] | None = None,
60
65
  ):
61
66
  super().__init__(work_dir, username)
62
67
  self.PS1 = CmdOutputMetadata.to_ps1_prompt()
68
+ self._env = normalize_terminal_env(env)
63
69
 
64
70
  def initialize(self) -> None:
65
71
  """Initialize the tmux terminal session."""
66
72
  if self._initialized:
67
73
  return
68
74
 
69
- env = sanitized_env()
75
+ env = build_terminal_env(self._env)
70
76
  # Disable interactive pagers (git, man, systemctl, ...) so commands that
71
77
  # auto-launch `less` on a TTY don't capture the pane and wedge the session.
72
78
  env.setdefault("GIT_PAGER", "cat")
@@ -10,14 +10,18 @@ import subprocess
10
10
  import threading
11
11
  import time
12
12
  from collections import deque
13
+ from collections.abc import Mapping
13
14
 
14
15
  from openhands.sdk.logger import get_logger
15
- from openhands.sdk.utils import sanitized_env
16
16
  from openhands.tools.terminal.constants import (
17
17
  CMD_OUTPUT_PS1_BEGIN,
18
18
  CMD_OUTPUT_PS1_END,
19
19
  HISTORY_LIMIT,
20
20
  )
21
+ from openhands.tools.terminal.env import (
22
+ build_terminal_env,
23
+ normalize_terminal_env,
24
+ )
21
25
  from openhands.tools.terminal.terminal.interface import (
22
26
  TerminalInterface,
23
27
  parse_ctrl_key,
@@ -70,6 +74,7 @@ class WindowsTerminal(TerminalInterface):
70
74
  work_dir: str,
71
75
  username: str | None = None,
72
76
  shell_path: str = "powershell.exe",
77
+ env: Mapping[str, str] | None = None,
73
78
  ):
74
79
  super().__init__(work_dir, username)
75
80
  self.process = None
@@ -77,6 +82,7 @@ class WindowsTerminal(TerminalInterface):
77
82
  self.output_lock = threading.Lock()
78
83
  self.reader_thread = None
79
84
  self.shell_path = shell_path
85
+ self._env = normalize_terminal_env(env)
80
86
  self._command_running_event = threading.Event()
81
87
  self._stop_reader = threading.Event()
82
88
  self._decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
@@ -96,7 +102,7 @@ class WindowsTerminal(TerminalInterface):
96
102
  creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
97
103
  creationflags |= getattr(subprocess, "CREATE_NO_WINDOW", 0)
98
104
 
99
- env = sanitized_env()
105
+ env = build_terminal_env(self._env)
100
106
  env.setdefault("PYTHONIOENCODING", "utf-8")
101
107
  env.setdefault("PYTHONUTF8", "1")
102
108
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.29.3
3
+ Version: 1.30.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -69,6 +69,7 @@ pyproject.toml
69
69
  ./openhands/tools/terminal/constants.py
70
70
  ./openhands/tools/terminal/definition.py
71
71
  ./openhands/tools/terminal/descriptions.py
72
+ ./openhands/tools/terminal/env.py
72
73
  ./openhands/tools/terminal/impl.py
73
74
  ./openhands/tools/terminal/metadata.py
74
75
  ./openhands/tools/terminal/timeout_policy.py
@@ -161,6 +162,7 @@ openhands/tools/terminal/__init__.py
161
162
  openhands/tools/terminal/constants.py
162
163
  openhands/tools/terminal/definition.py
163
164
  openhands/tools/terminal/descriptions.py
165
+ openhands/tools/terminal/env.py
164
166
  openhands/tools/terminal/impl.py
165
167
  openhands/tools/terminal/metadata.py
166
168
  openhands/tools/terminal/timeout_policy.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.29.3"
3
+ version = "1.30.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"