116 registry: dict[str, Any],
118 tool_sources: dict[str, str],
120 """Merge ``mod.TOOLS`` + ``mod.SCHEMAS`` into the accumulators.
122 Raises ``ValueError`` on any name collision, with both source
123 identifiers in the message so the offending plugin is unambiguous.
125 func =
"_merge_module_tools"
126 mod_tools = getattr(mod,
"TOOLS",
None)
127 mod_schemas = getattr(mod,
"SCHEMAS",
None)
129 for name
in mod_tools:
132 f
"Tool name collision: '{name}' is registered by "
133 f
"{tool_sources[name]} and also by {source}. "
134 f
"Tool names must be globally unique across the "
135 f
"filesystem scan and every entry point."
137 registry[name] = mod_tools[name]
138 tool_sources[name] = source
140 schemas.extend(mod_schemas)
144 f
"merged {source}: tools={len(mod_tools or {})} schemas={len(mod_schemas or [])}",
159 """Build the tool registry from the explicit module allowlist and entry points.
161 First-party pass imports the explicit :data:`_TOOL_MODULE_ALLOWLIST` (M8.0
162 WU-6 / G-2 — no ``tools/*.py`` glob, so an agent-written tool module is not
163 auto-imported); entry-point pass scans ``[project.entry-points."oc_chat.tools"]``
164 (Phase K.1b) **only under the permissive profile** (G-3: enforcing profiles
165 deny untrusted plugin code at startup). Both must declare ``TOOLS`` (dict) and
166 ``SCHEMAS`` (list) at module level.
168 Returns ``(registry, schemas)``. Raises :class:`ValueError` on any
169 tool-name collision. Entry-point import failures are logged at L1
170 and exposed via :func:`failed_plugins`.
172 func =
"build_tool_registry"
175 tool_sources: dict[str, str] = {}
177 _failed_plugins.clear()
185 for stem
in _TOOL_MODULE_ALLOWLIST:
186 mod_name = f
"oc_chat.tools.{stem}"
188 mod = importlib.import_module(mod_name)
189 except ImportError
as e:
191 _dbg(
"OC_CHAT.TOOLS", func, f
"VALIDATION_ERROR: {mod_name}: {e}", 3)
195 source=f
"allowlist:{mod_name}",
198 tool_sources=tool_sources,
206 if resolve_risk_profile() !=
"permissive":
207 _dbg(
"OC_CHAT.TOOLS", func,
208 "entry-point plugin pass skipped under an enforcing profile (G-3)", 2)
211 eps = importlib.metadata.entry_points(group=ENTRY_POINT_GROUP)
214 eps = importlib.metadata.entry_points().get(ENTRY_POINT_GROUP, [])
219 except Exception
as e:
223 err = f
"{type(e).__name__}: {e}"
224 _failed_plugins.append((ep.name, err))
228 f
"VALIDATION_ERROR: entry-point '{ep.name}' failed to load: {err}",
235 source=f
"entry-point:{ep.name}",
238 tool_sources=tool_sources,
240 _dbg(
"OC_CHAT.TOOLS", func, f
"loaded entry-point {ep.name}", 2)
245 f
"discovered tools={len(registry)} schemas={len(schemas)} failed_plugins={len(_failed_plugins)}",
248 return registry, schemas