openhands-tools 1.58.2__tar.gz → 1.59.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 (111) hide show
  1. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/PKG-INFO +1 -1
  2. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/child_conversation/definition.py +7 -4
  3. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/task/definition.py +2 -1
  4. openhands_tools-1.59.0/openhands/tools/terminal/env.py +88 -0
  5. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +3 -0
  6. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/tmux_terminal.py +3 -0
  7. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands_tools.egg-info/PKG-INFO +1 -1
  8. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/pyproject.toml +1 -1
  9. openhands_tools-1.58.2/openhands/tools/terminal/env.py +0 -39
  10. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/__init__.py +0 -0
  11. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/apply_patch/__init__.py +0 -0
  12. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/apply_patch/core.py +0 -0
  13. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/apply_patch/definition.py +0 -0
  14. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  15. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/ask_oracle/definition.py +0 -0
  16. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/ask_oracle/impl.py +0 -0
  17. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/__init__.py +0 -0
  18. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/definition.py +0 -0
  19. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/event_storage.py +0 -0
  20. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/impl.py +0 -0
  21. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  22. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/recording.py +0 -0
  23. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/browser_use/server.py +0 -0
  24. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/child_conversation/__init__.py +0 -0
  25. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/child_conversation/impl.py +0 -0
  26. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/delegate/__init__.py +0 -0
  27. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/delegate/definition.py +0 -0
  28. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/delegate/impl.py +0 -0
  29. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/delegate/visualizer.py +0 -0
  30. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/__init__.py +0 -0
  31. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/definition.py +0 -0
  32. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/editor.py +0 -0
  33. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/exceptions.py +0 -0
  34. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/impl.py +0 -0
  35. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  36. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/config.py +0 -0
  37. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  38. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  39. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  40. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  41. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/history.py +0 -0
  42. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  43. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/__init__.py +0 -0
  44. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  45. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/edit/definition.py +0 -0
  46. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/edit/impl.py +0 -0
  47. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/file_change.py +0 -0
  48. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  49. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  50. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  51. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  52. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  53. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  54. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  55. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  56. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  57. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/glob/__init__.py +0 -0
  58. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/glob/definition.py +0 -0
  59. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/glob/impl.py +0 -0
  60. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/grep/__init__.py +0 -0
  61. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/grep/definition.py +0 -0
  62. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/grep/impl.py +0 -0
  63. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  64. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  65. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  66. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/__init__.py +0 -0
  67. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/default.py +0 -0
  68. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/gemini.py +0 -0
  69. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/gpt5.py +0 -0
  70. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/planning.py +0 -0
  71. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  72. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  73. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/subagents/default.md +0 -0
  74. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  75. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/preset/task_outcome.py +0 -0
  76. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/py.typed +0 -0
  77. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/task/__init__.py +0 -0
  78. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/task/impl.py +0 -0
  79. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/task/manager.py +0 -0
  80. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/task_tracker/__init__.py +0 -0
  81. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/task_tracker/definition.py +0 -0
  82. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/__init__.py +0 -0
  83. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/constants.py +0 -0
  84. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/definition.py +0 -0
  85. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/descriptions.py +0 -0
  86. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/impl.py +0 -0
  87. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/metadata.py +0 -0
  88. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  89. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  90. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  91. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/process_priority.py +0 -0
  92. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  93. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  94. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
  95. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  96. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  97. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/utils/command.py +0 -0
  98. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  99. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/tom_consult/__init__.py +0 -0
  100. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/tom_consult/definition.py +0 -0
  101. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/tom_consult/executor.py +0 -0
  102. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/utils/__init__.py +0 -0
  103. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/utils/timeout.py +0 -0
  104. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/workflow/__init__.py +0 -0
  105. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/workflow/definition.py +0 -0
  106. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands/tools/workflow/impl.py +0 -0
  107. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
  108. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  109. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands_tools.egg-info/requires.txt +0 -0
  110. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/openhands_tools.egg-info/top_level.txt +0 -0
  111. {openhands_tools-1.58.2 → openhands_tools-1.59.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.58.2
3
+ Version: 1.59.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
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, ClassVar, Literal, Self
6
6
  from pydantic import Field
7
7
  from rich.text import Text
8
8
 
9
+ from openhands.sdk.event.types import UnmaskedID
9
10
  from openhands.sdk.tool.registry import register_tool
10
11
  from openhands.sdk.tool.tool import (
11
12
  Action,
@@ -36,7 +37,8 @@ class StartChildConversationAction(Action):
36
37
  default="worktree",
37
38
  description=(
38
39
  "'worktree' gives the child its own git worktree and branch when the "
39
- "workspace is a git repository; 'shared' runs it in the same directory."
40
+ "workspace has a usable git base commit; otherwise it shares the "
41
+ "directory. 'shared' always runs it in the same directory."
40
42
  ),
41
43
  )
42
44
 
@@ -54,8 +56,8 @@ class StartChildConversationAction(Action):
54
56
  class StartChildConversationObservation(Observation):
55
57
  """Result of a child conversation launch."""
56
58
 
57
- conversation_id: str | None = None
58
- parent_conversation_id: str | None = None
59
+ conversation_id: UnmaskedID | None = None
60
+ parent_conversation_id: UnmaskedID | None = None
59
61
  status: str | None = None
60
62
  title: str | None = None
61
63
  url: str | None = None
@@ -92,7 +94,8 @@ _DESCRIPTION = (
92
94
  "conversation's history, so include the goal, relevant paths, constraints "
93
95
  "and the expected deliverable. Use `title` to name it. `isolation` controls "
94
96
  "the workspace: `worktree` (default) gives the child its own git worktree "
95
- "and branch when the workspace is a git repository; the worktree starts "
97
+ "and branch when the workspace has a usable git base commit; otherwise "
98
+ "it shares the directory and reports `shared` isolation. The worktree starts "
96
99
  "from the repository's default branch, not from this conversation's branch "
97
100
  "or uncommitted changes, so use `shared` when the child must see them. "
98
101
  "`shared` runs it in the same directory; backends that manage workspaces "
@@ -17,6 +17,7 @@ from pydantic.json_schema import SkipJsonSchema
17
17
  from rich.text import Text
18
18
 
19
19
  from openhands.sdk import ImageContent, TextContent
20
+ from openhands.sdk.event.types import UnmaskedID
20
21
  from openhands.sdk.logger import get_logger
21
22
  from openhands.sdk.subagent import (
22
23
  AgentDefinition,
@@ -75,7 +76,7 @@ class TaskAction(Action):
75
76
  class TaskObservation(Observation):
76
77
  """Observation from a task execution."""
77
78
 
78
- task_id: str = Field(description="The unique identifier of the task.")
79
+ task_id: UnmaskedID = Field(description="The unique identifier of the task.")
79
80
  subagent: str = Field(description="The subagent of the task.")
80
81
  status: str = Field(description="The status of the task.")
81
82
 
@@ -0,0 +1,88 @@
1
+ """Environment helpers for terminal backends."""
2
+
3
+ import codecs
4
+ import locale
5
+ import os
6
+ import re
7
+ from collections.abc import Mapping
8
+
9
+ from openhands.sdk.logger import get_logger
10
+ from openhands.sdk.utils import sanitized_env
11
+
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
16
+ UTF8_FALLBACK_LOCALE = "C.UTF-8"
17
+
18
+
19
+ def normalize_terminal_env(
20
+ extra_env: Mapping[str, str] | None,
21
+ ) -> dict[str, str] | None:
22
+ """Validate and copy client-provided terminal environment variables."""
23
+ if extra_env is None:
24
+ return None
25
+
26
+ normalized: dict[str, str] = {}
27
+ for key, value in extra_env.items():
28
+ if not isinstance(key, str) or not ENV_VAR_NAME_RE.match(key):
29
+ raise ValueError(f"Invalid terminal environment variable name: {key!r}")
30
+ if not isinstance(value, str):
31
+ raise TypeError(
32
+ "Terminal environment variable values must be strings: "
33
+ f"{key!r}={value!r}"
34
+ )
35
+ normalized[key] = value
36
+ return normalized
37
+
38
+
39
+ def build_terminal_env(extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
40
+ """Return the sanitized process environment plus client-provided overrides."""
41
+ env = sanitized_env()
42
+ normalized = normalize_terminal_env(extra_env)
43
+ if normalized:
44
+ env.update(normalized)
45
+ env = sanitized_env(env)
46
+ return env
47
+
48
+
49
+ def has_utf8_locale(environ: Mapping[str, str]) -> bool:
50
+ """Apply tmux's check: TMUX is set, or the first non-empty of LC_ALL,
51
+ LC_CTYPE, LANG names UTF-8."""
52
+ if "TMUX" in environ:
53
+ return True
54
+ for name in ("LC_ALL", "LC_CTYPE", "LANG"):
55
+ value = environ.get(name)
56
+ if value:
57
+ return "utf-8" in value.lower() or "utf8" in value.lower()
58
+ return False
59
+
60
+
61
+ def ensure_utf8_locale() -> tuple[str, str | None] | None:
62
+ """Fall back to a UTF-8 locale when the process has none.
63
+
64
+ Outside a UTF-8 locale, tmux replaces non-ASCII characters in format output
65
+ with ``_``, which breaks libtmux's parsing of the ``␞`` separator. As in
66
+ PEP 538, only LC_CTYPE is set unless LC_ALL overrides it.
67
+
68
+ Return the variable that was set and its previous value, or None.
69
+ """
70
+ if has_utf8_locale(os.environ):
71
+ return None
72
+ # libtmux decodes tmux output with the encoding Python picked at startup,
73
+ # so a locale set now cannot help if that encoding is not UTF-8.
74
+ encoding = locale.getpreferredencoding(False)
75
+ if codecs.lookup(encoding).name != "utf-8":
76
+ logger.warning(
77
+ "tmux needs a UTF-8 locale, but Python decodes %s; start the "
78
+ "process with a UTF-8 locale or PYTHONUTF8=1",
79
+ encoding,
80
+ )
81
+ return None
82
+ name = "LC_ALL" if os.environ.get("LC_ALL") else "LC_CTYPE"
83
+ previous = os.environ.get(name)
84
+ os.environ[name] = UTF8_FALLBACK_LOCALE
85
+ logger.warning(
86
+ "No UTF-8 locale set; using %s=%s for tmux", name, UTF8_FALLBACK_LOCALE
87
+ )
88
+ return name, previous
@@ -26,6 +26,7 @@ from openhands.tools.terminal.constants import (
26
26
  )
27
27
  from openhands.tools.terminal.env import (
28
28
  build_terminal_env,
29
+ ensure_utf8_locale,
29
30
  normalize_terminal_env,
30
31
  )
31
32
  from openhands.tools.terminal.terminal.process_priority import (
@@ -123,6 +124,8 @@ class TmuxPanePool:
123
124
  if self._initialized:
124
125
  return
125
126
 
127
+ # Before building env, so the shells get the same locale as tmux.
128
+ ensure_utf8_locale()
126
129
  env = build_terminal_env(self.env)
127
130
  session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
128
131
  # Stale handles must not reach another pool after tmux restarts.
@@ -25,6 +25,7 @@ from openhands.tools.terminal.constants import (
25
25
  )
26
26
  from openhands.tools.terminal.env import (
27
27
  build_terminal_env,
28
+ ensure_utf8_locale,
28
29
  normalize_terminal_env,
29
30
  )
30
31
  from openhands.tools.terminal.metadata import CmdOutputMetadata
@@ -214,6 +215,8 @@ class TmuxTerminal(TerminalInterface):
214
215
  if self._initialized:
215
216
  return
216
217
 
218
+ # Before building env, so the shell gets the same locale as tmux.
219
+ ensure_utf8_locale()
217
220
  env = build_terminal_env(self._env)
218
221
  # Disable interactive pagers (git, man, systemctl, ...) so commands that
219
222
  # auto-launch `less` on a TTY don't capture the pane and wedge the session.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.58.2
3
+ Version: 1.59.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.58.2"
3
+ version = "1.59.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -1,39 +0,0 @@
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