galet-tools 0.1.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 (59) hide show
  1. galet_tools-0.1.0/LICENSE +21 -0
  2. galet_tools-0.1.0/PKG-INFO +190 -0
  3. galet_tools-0.1.0/README.md +166 -0
  4. galet_tools-0.1.0/pyproject.toml +40 -0
  5. galet_tools-0.1.0/setup.cfg +4 -0
  6. galet_tools-0.1.0/src/galet_tools/__init__.py +25 -0
  7. galet_tools-0.1.0/src/galet_tools/discovery.py +69 -0
  8. galet_tools-0.1.0/src/galet_tools/framework/__init__.py +12 -0
  9. galet_tools-0.1.0/src/galet_tools/framework/handler_registry.py +195 -0
  10. galet_tools-0.1.0/src/galet_tools/framework/handler_v2.py +34 -0
  11. galet_tools-0.1.0/src/galet_tools/framework/schema_handler_v2.py +178 -0
  12. galet_tools-0.1.0/src/galet_tools/host/__init__.py +0 -0
  13. galet_tools-0.1.0/src/galet_tools/host/adapters.py +58 -0
  14. galet_tools-0.1.0/src/galet_tools/host/config.py +166 -0
  15. galet_tools-0.1.0/src/galet_tools/host/security.py +103 -0
  16. galet_tools-0.1.0/src/galet_tools/mcp/__init__.py +0 -0
  17. galet_tools-0.1.0/src/galet_tools/mcp/server.py +227 -0
  18. galet_tools-0.1.0/src/galet_tools/mcp/smoke.py +144 -0
  19. galet_tools-0.1.0/src/galet_tools/mcp/tool_adapter.py +143 -0
  20. galet_tools-0.1.0/src/galet_tools/ports/__init__.py +0 -0
  21. galet_tools-0.1.0/src/galet_tools/ports/config_provider.py +20 -0
  22. galet_tools-0.1.0/src/galet_tools/ports/sandbox_root_resolver.py +15 -0
  23. galet_tools-0.1.0/src/galet_tools/ports/security_policy.py +19 -0
  24. galet_tools-0.1.0/src/galet_tools/ports/storage_location_resolver.py +26 -0
  25. galet_tools-0.1.0/src/galet_tools/register_generic_defaults.py +23 -0
  26. galet_tools-0.1.0/src/galet_tools/tools/__init__.py +0 -0
  27. galet_tools-0.1.0/src/galet_tools/tools/command_execution_handler2.py +488 -0
  28. galet_tools-0.1.0/src/galet_tools/tools/file_load_handler2.py +390 -0
  29. galet_tools-0.1.0/src/galet_tools/tools/file_save_handler.py +279 -0
  30. galet_tools-0.1.0/src/galet_tools/tools/generate_image_handler.py +188 -0
  31. galet_tools-0.1.0/src/galet_tools/tools/generate_svg_handler.py +352 -0
  32. galet_tools-0.1.0/src/galet_tools/tools/patch_apply_handler.py +478 -0
  33. galet_tools-0.1.0/src/galet_tools/tools/repo_index_handler.py +78 -0
  34. galet_tools-0.1.0/src/galet_tools/tools/repo_search_handler.py +69 -0
  35. galet_tools-0.1.0/src/galet_tools/tools/repository_handler_support.py +43 -0
  36. galet_tools-0.1.0/src/galet_tools/tools/repository_index.py +226 -0
  37. galet_tools-0.1.0/src/galet_tools.egg-info/PKG-INFO +190 -0
  38. galet_tools-0.1.0/src/galet_tools.egg-info/SOURCES.txt +57 -0
  39. galet_tools-0.1.0/src/galet_tools.egg-info/dependency_links.txt +1 -0
  40. galet_tools-0.1.0/src/galet_tools.egg-info/entry_points.txt +3 -0
  41. galet_tools-0.1.0/src/galet_tools.egg-info/requires.txt +5 -0
  42. galet_tools-0.1.0/src/galet_tools.egg-info/top_level.txt +1 -0
  43. galet_tools-0.1.0/tests/test_command_execution_handler2.py +144 -0
  44. galet_tools-0.1.0/tests/test_discovery.py +128 -0
  45. galet_tools-0.1.0/tests/test_execute_command_fail_fast.py +84 -0
  46. galet_tools-0.1.0/tests/test_file_load_handler2.py +272 -0
  47. galet_tools-0.1.0/tests/test_file_save_handler.py +143 -0
  48. galet_tools-0.1.0/tests/test_generate_image_handler.py +111 -0
  49. galet_tools-0.1.0/tests/test_generate_svg_handler.py +195 -0
  50. galet_tools-0.1.0/tests/test_handler_registry_factory.py +69 -0
  51. galet_tools-0.1.0/tests/test_host_config.py +159 -0
  52. galet_tools-0.1.0/tests/test_mcp_server_dispatch.py +139 -0
  53. galet_tools-0.1.0/tests/test_mcp_server_dispatch_errors.py +78 -0
  54. galet_tools-0.1.0/tests/test_mcp_server_scope.py +116 -0
  55. galet_tools-0.1.0/tests/test_patch_apply_handler.py +221 -0
  56. galet_tools-0.1.0/tests/test_permission_filtering.py +103 -0
  57. galet_tools-0.1.0/tests/test_register_generic_defaults.py +91 -0
  58. galet_tools-0.1.0/tests/test_repository_navigation_handlers.py +78 -0
  59. galet_tools-0.1.0/tests/test_tool_adapter.py +248 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 junwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: galet-tools
3
+ Version: 0.1.0
4
+ Summary: Self-contained generic tool handlers and handler framework extracted from the Lucy monorepo
5
+ Author-email: junwin <jdunwin@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/junwin/galet-tools
8
+ Project-URL: Repository, https://github.com/junwin/galet-tools
9
+ Keywords: tools,handlers,files,svg,image-generation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: Pillow
20
+ Requires-Dist: pydantic>=2
21
+ Provides-Extra: mcp
22
+ Requires-Dist: mcp==2.1.1; extra == "mcp"
23
+ Dynamic: license-file
24
+
25
+ # galet-tools
26
+
27
+ Self-contained generic tool handlers and the handler framework they run on.
28
+
29
+ ## Lineage
30
+
31
+ This package is extracted from the Lucy monorepo (`src/handlers`) as described
32
+ in the Lucy design doc `software/ai/lucy/design/handlers-extraction.md` and
33
+ the plan `software/ai/lucy/plan/handlers-extraction.md`.
34
+
35
+ It is a fresh repository with no shared git history. Handler logic moves
36
+ verbatim; the only intentional changes are the import namespace
37
+ (`src.handlers.*` -> `galet_tools.*`) and the configuration boundary: tools
38
+ depend on narrow port interfaces owned by this package (`ConfigProvider`,
39
+ `StorageLocationResolver`, `SandboxRootResolver`, `SecurityPolicy`, ...),
40
+ never on Lucy's `ConfigManager`. Lucy implements those ports at its
41
+ composition root.
42
+
43
+ ## Features
44
+
45
+ - **Handler framework** — `HandlerV2` ABC, `HandlerRegistry`, schema
46
+ handling, and permission filtering.
47
+ - **Generic tools** — `file_load`, `file_save`, `patch_apply`,
48
+ `execute_command`, `generate_svg`, and `generate_image`, registered
49
+ under their existing names. `file_load` supports bounded line-range reads.
50
+ - **Ports** — sibling-owned interfaces the host implements, keeping the tools
51
+ free of any host dependency.
52
+ - **Handler discovery** — installed packages can contribute handlers through
53
+ Python package entry points without changing Lucy or galet-tools.
54
+
55
+ ## Quick start
56
+
57
+ ```bash
58
+ python -m venv .venv
59
+ .venv/bin/pip install -e .
60
+ ```
61
+
62
+ Run the suite:
63
+
64
+ ```bash
65
+ python -m pytest -q
66
+ ```
67
+
68
+ ## Using the registry in a host
69
+
70
+ Galet owns the handler contract and base registry. A host such as Lucy composes
71
+ the generic handlers, its own host-specific handlers, and installed extension
72
+ packages:
73
+
74
+ ```python
75
+ from galet_tools import HandlerRegistry, register_installed_handlers
76
+ from galet_tools.register_generic_defaults import register_generic_defaults
77
+
78
+ registry = HandlerRegistry()
79
+ register_generic_defaults(registry)
80
+ register_lucy_handlers(registry)
81
+ register_installed_handlers(registry)
82
+ ```
83
+
84
+ Discovery adds installed capabilities to the registry; it does not grant them
85
+ to an agent. The host should continue to apply its agent and context allowlists
86
+ when selecting tool definitions.
87
+
88
+ ### Creating a handler extension
89
+
90
+ An extension package declares a registrar in its `pyproject.toml`:
91
+
92
+ ```toml
93
+ [project.entry-points."galet_tools.handlers"]
94
+ web = "galet_web_tools:register"
95
+ ```
96
+
97
+ The target is a callable that receives the shared registry:
98
+
99
+ ```python
100
+ def register(registry):
101
+ registry.register(SearchHandler)
102
+ registry.register(FetchHandler)
103
+ ```
104
+
105
+ Entry points are loaded in deterministic name order. A plugin that cannot be
106
+ loaded, does not resolve to a callable, or fails during registration raises
107
+ `HandlerPluginError` with the responsible entry-point name. This is
108
+ intentionally fail-fast so a host cannot start with an unexpectedly incomplete
109
+ tool set.
110
+
111
+ ## MCP server
112
+
113
+ An optional MCP (Model Context Protocol) server exposes the six generic
114
+ tools over streamable HTTP. Install the extra:
115
+
116
+ ```bash
117
+ .venv/bin/pip install -e '.[mcp]'
118
+ ```
119
+
120
+ Copy the example config and start the server:
121
+
122
+ ```bash
123
+ cp galet-mcp.example.json galet-mcp.json
124
+ galet-tools-mcp
125
+ ```
126
+
127
+ `galet-tools-mcp` reads `galet-mcp.json` from the current directory by
128
+ default; point it elsewhere with `--config <path>`.
129
+
130
+ ### Configuration
131
+
132
+ `galet-mcp.json` mirrors `galet-mcp.example.json`. Required keys are
133
+ `account`, `storage_root`, `storage_namespace`, and `sandbox_root`; a
134
+ null, blank, or (for the two path keys) relative value fails validation.
135
+ Optional keys left out or set to null fall back to their defaults:
136
+
137
+ - `allowlist` — tools to expose; default is all six (`file_load`,
138
+ `file_save`, `patch_apply`, `execute_command`, `generate_svg`,
139
+ `generate_image`).
140
+ - `external_roots` — named external roots the tools may access; default
141
+ none.
142
+ - `host` — default `127.0.0.1`; loopback only.
143
+ - `port` — default `8765`.
144
+ - `fonts` — font paths for the generators; default none.
145
+
146
+ ### What it exposes
147
+
148
+ `tools/list` and `tools/call` over streamable HTTP at
149
+ `http://127.0.0.1:8765/mcp`.
150
+
151
+ ### Smoke-test from the CLI
152
+
153
+ `galet-tools-smoke` is the end-to-end CLI client: it connects over HTTP
154
+ (streamable HTTP) to a running galet MCP server using the official MCP SDK
155
+ and performs real `tools/list` and `tools/call` round trips.
156
+
157
+ ```bash
158
+ galet-tools-smoke list
159
+ galet-tools-smoke list --json
160
+ galet-tools-smoke call <tool> '<json args>'
161
+ ```
162
+
163
+ The default URL is `http://127.0.0.1:8765/mcp`; override it with `--url`.
164
+ The default read timeout is 30s; override it with `--timeout`.
165
+
166
+ Example round trip (storage root from `galet-mcp.json`):
167
+
168
+ ```bash
169
+ galet-tools-smoke call file_save '{"path":"smoke/hello.txt","file_content":"hello galet smoke"}'
170
+ galet-tools-smoke call file_load '{"path":"smoke/hello.txt"}'
171
+ galet-tools-smoke call execute_command '{"command":"pwd","working_directory":"."}'
172
+ ```
173
+
174
+ Exit codes: `0` ok; `1` tool reported an error (`isError`); `2` usage or
175
+ bad JSON; `3` could not reach the server.
176
+
177
+ ### Fail-closed behaviour
178
+
179
+ The server refuses to start when the allowlist is empty or has no overlap
180
+ with the registered tools, and only the advertised tools are callable.
181
+ Unknown tool names in the allowlist fail validation.
182
+
183
+ ### Security
184
+
185
+ Binding is loopback-only by design: `host` must resolve to a loopback
186
+ address. Remote access needs a separate reverse-proxy design.
187
+
188
+ ## License
189
+
190
+ MIT — see `LICENSE`.
@@ -0,0 +1,166 @@
1
+ # galet-tools
2
+
3
+ Self-contained generic tool handlers and the handler framework they run on.
4
+
5
+ ## Lineage
6
+
7
+ This package is extracted from the Lucy monorepo (`src/handlers`) as described
8
+ in the Lucy design doc `software/ai/lucy/design/handlers-extraction.md` and
9
+ the plan `software/ai/lucy/plan/handlers-extraction.md`.
10
+
11
+ It is a fresh repository with no shared git history. Handler logic moves
12
+ verbatim; the only intentional changes are the import namespace
13
+ (`src.handlers.*` -> `galet_tools.*`) and the configuration boundary: tools
14
+ depend on narrow port interfaces owned by this package (`ConfigProvider`,
15
+ `StorageLocationResolver`, `SandboxRootResolver`, `SecurityPolicy`, ...),
16
+ never on Lucy's `ConfigManager`. Lucy implements those ports at its
17
+ composition root.
18
+
19
+ ## Features
20
+
21
+ - **Handler framework** — `HandlerV2` ABC, `HandlerRegistry`, schema
22
+ handling, and permission filtering.
23
+ - **Generic tools** — `file_load`, `file_save`, `patch_apply`,
24
+ `execute_command`, `generate_svg`, and `generate_image`, registered
25
+ under their existing names. `file_load` supports bounded line-range reads.
26
+ - **Ports** — sibling-owned interfaces the host implements, keeping the tools
27
+ free of any host dependency.
28
+ - **Handler discovery** — installed packages can contribute handlers through
29
+ Python package entry points without changing Lucy or galet-tools.
30
+
31
+ ## Quick start
32
+
33
+ ```bash
34
+ python -m venv .venv
35
+ .venv/bin/pip install -e .
36
+ ```
37
+
38
+ Run the suite:
39
+
40
+ ```bash
41
+ python -m pytest -q
42
+ ```
43
+
44
+ ## Using the registry in a host
45
+
46
+ Galet owns the handler contract and base registry. A host such as Lucy composes
47
+ the generic handlers, its own host-specific handlers, and installed extension
48
+ packages:
49
+
50
+ ```python
51
+ from galet_tools import HandlerRegistry, register_installed_handlers
52
+ from galet_tools.register_generic_defaults import register_generic_defaults
53
+
54
+ registry = HandlerRegistry()
55
+ register_generic_defaults(registry)
56
+ register_lucy_handlers(registry)
57
+ register_installed_handlers(registry)
58
+ ```
59
+
60
+ Discovery adds installed capabilities to the registry; it does not grant them
61
+ to an agent. The host should continue to apply its agent and context allowlists
62
+ when selecting tool definitions.
63
+
64
+ ### Creating a handler extension
65
+
66
+ An extension package declares a registrar in its `pyproject.toml`:
67
+
68
+ ```toml
69
+ [project.entry-points."galet_tools.handlers"]
70
+ web = "galet_web_tools:register"
71
+ ```
72
+
73
+ The target is a callable that receives the shared registry:
74
+
75
+ ```python
76
+ def register(registry):
77
+ registry.register(SearchHandler)
78
+ registry.register(FetchHandler)
79
+ ```
80
+
81
+ Entry points are loaded in deterministic name order. A plugin that cannot be
82
+ loaded, does not resolve to a callable, or fails during registration raises
83
+ `HandlerPluginError` with the responsible entry-point name. This is
84
+ intentionally fail-fast so a host cannot start with an unexpectedly incomplete
85
+ tool set.
86
+
87
+ ## MCP server
88
+
89
+ An optional MCP (Model Context Protocol) server exposes the six generic
90
+ tools over streamable HTTP. Install the extra:
91
+
92
+ ```bash
93
+ .venv/bin/pip install -e '.[mcp]'
94
+ ```
95
+
96
+ Copy the example config and start the server:
97
+
98
+ ```bash
99
+ cp galet-mcp.example.json galet-mcp.json
100
+ galet-tools-mcp
101
+ ```
102
+
103
+ `galet-tools-mcp` reads `galet-mcp.json` from the current directory by
104
+ default; point it elsewhere with `--config <path>`.
105
+
106
+ ### Configuration
107
+
108
+ `galet-mcp.json` mirrors `galet-mcp.example.json`. Required keys are
109
+ `account`, `storage_root`, `storage_namespace`, and `sandbox_root`; a
110
+ null, blank, or (for the two path keys) relative value fails validation.
111
+ Optional keys left out or set to null fall back to their defaults:
112
+
113
+ - `allowlist` — tools to expose; default is all six (`file_load`,
114
+ `file_save`, `patch_apply`, `execute_command`, `generate_svg`,
115
+ `generate_image`).
116
+ - `external_roots` — named external roots the tools may access; default
117
+ none.
118
+ - `host` — default `127.0.0.1`; loopback only.
119
+ - `port` — default `8765`.
120
+ - `fonts` — font paths for the generators; default none.
121
+
122
+ ### What it exposes
123
+
124
+ `tools/list` and `tools/call` over streamable HTTP at
125
+ `http://127.0.0.1:8765/mcp`.
126
+
127
+ ### Smoke-test from the CLI
128
+
129
+ `galet-tools-smoke` is the end-to-end CLI client: it connects over HTTP
130
+ (streamable HTTP) to a running galet MCP server using the official MCP SDK
131
+ and performs real `tools/list` and `tools/call` round trips.
132
+
133
+ ```bash
134
+ galet-tools-smoke list
135
+ galet-tools-smoke list --json
136
+ galet-tools-smoke call <tool> '<json args>'
137
+ ```
138
+
139
+ The default URL is `http://127.0.0.1:8765/mcp`; override it with `--url`.
140
+ The default read timeout is 30s; override it with `--timeout`.
141
+
142
+ Example round trip (storage root from `galet-mcp.json`):
143
+
144
+ ```bash
145
+ galet-tools-smoke call file_save '{"path":"smoke/hello.txt","file_content":"hello galet smoke"}'
146
+ galet-tools-smoke call file_load '{"path":"smoke/hello.txt"}'
147
+ galet-tools-smoke call execute_command '{"command":"pwd","working_directory":"."}'
148
+ ```
149
+
150
+ Exit codes: `0` ok; `1` tool reported an error (`isError`); `2` usage or
151
+ bad JSON; `3` could not reach the server.
152
+
153
+ ### Fail-closed behaviour
154
+
155
+ The server refuses to start when the allowlist is empty or has no overlap
156
+ with the registered tools, and only the advertised tools are callable.
157
+ Unknown tool names in the allowlist fail validation.
158
+
159
+ ### Security
160
+
161
+ Binding is loopback-only by design: `host` must resolve to a loopback
162
+ address. Remote access needs a separate reverse-proxy design.
163
+
164
+ ## License
165
+
166
+ MIT — see `LICENSE`.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "galet-tools"
7
+ version = "0.1.0"
8
+ description = "Self-contained generic tool handlers and handler framework extracted from the Lucy monorepo"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "junwin", email = "jdunwin@gmail.com" }]
13
+ keywords = ["tools", "handlers", "files", "svg", "image-generation"]
14
+ dependencies = ["Pillow", "pydantic>=2"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ mcp = ["mcp==2.1.1"]
26
+
27
+ [project.scripts]
28
+ galet-tools-mcp = "galet_tools.mcp.server:main"
29
+ galet-tools-smoke = "galet_tools.mcp.smoke:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/junwin/galet-tools"
33
+ Repository = "https://github.com/junwin/galet-tools"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ from .discovery import (
2
+ ENTRY_POINT_GROUP,
3
+ HandlerPluginError,
4
+ register_installed_handlers,
5
+ )
6
+ from .framework import (
7
+ ErrorCode,
8
+ HandlerRegistry,
9
+ HandlerV2,
10
+ ResultEnvelope,
11
+ SchemaHandlerV2,
12
+ filter_eligible_tool_defs,
13
+ )
14
+
15
+ __all__ = [
16
+ "ENTRY_POINT_GROUP",
17
+ "ErrorCode",
18
+ "HandlerPluginError",
19
+ "HandlerRegistry",
20
+ "HandlerV2",
21
+ "ResultEnvelope",
22
+ "SchemaHandlerV2",
23
+ "filter_eligible_tool_defs",
24
+ "register_installed_handlers",
25
+ ]
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import entry_points
4
+ from typing import Iterable
5
+
6
+ from galet_tools.framework.handler_registry import HandlerRegistry
7
+
8
+ ENTRY_POINT_GROUP = "galet_tools.handlers"
9
+
10
+
11
+ class HandlerPluginError(RuntimeError):
12
+ """Raised when an installed handler plugin cannot be registered."""
13
+
14
+
15
+ def _handler_entry_points(group: str) -> Iterable[object]:
16
+ """Return handler entry points in deterministic order.
17
+
18
+ The fallback supports the mapping-style object returned by older
19
+ importlib-metadata releases, even though galet-tools itself requires
20
+ Python 3.11 or newer.
21
+ """
22
+ discovered = entry_points()
23
+ if hasattr(discovered, "select"):
24
+ selected = discovered.select(group=group)
25
+ else: # pragma: no cover - compatibility with old importlib-metadata
26
+ selected = discovered.get(group, ())
27
+ return sorted(selected, key=lambda ep: (ep.name, ep.value))
28
+
29
+
30
+ def register_installed_handlers(
31
+ registry: HandlerRegistry,
32
+ *,
33
+ group: str = ENTRY_POINT_GROUP,
34
+ ) -> list[str]:
35
+ """Load installed handler registrars and apply them to ``registry``.
36
+
37
+ Packages contribute a callable through the ``galet_tools.handlers``
38
+ entry-point group. The callable receives the registry and registers one
39
+ or more HandlerV2 classes. Broken plugins fail startup with the entry-point
40
+ name included, rather than silently removing tools from the host.
41
+
42
+ Returns the entry-point names in the order they were invoked.
43
+ """
44
+ loaded: list[str] = []
45
+ for entry_point in _handler_entry_points(group):
46
+ try:
47
+ registrar = entry_point.load()
48
+ except Exception as exc:
49
+ raise HandlerPluginError(
50
+ f"Could not load handler plugin {entry_point.name!r} "
51
+ f"({entry_point.value})"
52
+ ) from exc
53
+
54
+ if not callable(registrar):
55
+ raise HandlerPluginError(
56
+ f"Handler plugin {entry_point.name!r} ({entry_point.value}) "
57
+ "must resolve to a callable registrar"
58
+ )
59
+
60
+ try:
61
+ registrar(registry)
62
+ except Exception as exc:
63
+ raise HandlerPluginError(
64
+ f"Handler plugin {entry_point.name!r} ({entry_point.value}) "
65
+ "failed during registration"
66
+ ) from exc
67
+ loaded.append(entry_point.name)
68
+
69
+ return loaded
@@ -0,0 +1,12 @@
1
+ from .handler_registry import HandlerRegistry, filter_eligible_tool_defs
2
+ from .handler_v2 import HandlerV2
3
+ from .schema_handler_v2 import ErrorCode, ResultEnvelope, SchemaHandlerV2
4
+
5
+ __all__ = [
6
+ "ErrorCode",
7
+ "HandlerRegistry",
8
+ "HandlerV2",
9
+ "ResultEnvelope",
10
+ "SchemaHandlerV2",
11
+ "filter_eligible_tool_defs",
12
+ ]