From 7ff9c9ba5cbc13f02be401ac93bfd7561a16a00c Mon Sep 17 00:00:00 2001 From: Marcelo Date: Sun, 2 Aug 2026 21:55:21 -0300 Subject: [PATCH 1/2] chore(sdk): validate reserved tool names before secret lookup and adapter calls --- .../agenta/sdk/agents/tools/resolver.py | 51 ++++++++++++++--- .../pytest/unit/agents/tools/test_resolver.py | 56 +++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/sdks/python/agenta/sdk/agents/tools/resolver.py b/sdks/python/agenta/sdk/agents/tools/resolver.py index 035372c1a4..187714840c 100644 --- a/sdks/python/agenta/sdk/agents/tools/resolver.py +++ b/sdks/python/agenta/sdk/agents/tools/resolver.py @@ -87,16 +87,52 @@ def _build_client_tool_spec(*, tool_config: ClientToolConfig) -> ClientToolSpec: ) +def _check_tool_name(name: str, seen: set[str]) -> None: + # The harness registers custom tools by name beside its built-ins, so a same-named custom + # tool would silently replace the built-in the platform activates on every run. + if name.strip().lower() in PI_BUILTIN_TOOL_NAMES: + raise ReservedToolNameError(name) + if name in seen: + raise DuplicateToolNameError(name) + seen.add(name) + + +def _declared_config_name(tool_config: ToolConfig) -> Optional[str]: + """The tool name a config declares, if any, before adapters turn it into specs. + + Returns ``None`` for legacy ``builtin`` entries (ignored, never produce specs), for + gateway configs that omit ``name`` (the adapter then derives ``integration__action``), + and for anything that is not a recognized tool config (left to downstream validation). + """ + if isinstance(tool_config, ReferenceToolConfig): + return tool_config.tool_name + if isinstance(tool_config, PlatformToolConfig): + return tool_config.op + if isinstance(tool_config, (CodeToolConfig, ClientToolConfig, GatewayToolConfig)): + return tool_config.name + return None + + +def _validate_declared_config_names(tool_configs: Sequence[ToolConfig]) -> None: + """Reject reserved or duplicate declared tool names up front. + + Runs before any secret lookup or adapter call so a payload that will always be refused + never makes the resolver do work, and a reserved-named ``code`` tool with a missing + secret surfaces ``ReservedToolNameError`` instead of ``MissingToolSecretError``. + Adapter-produced spec names (e.g. a gateway ``integration__action`` fallback) are not + visible here and stay covered by the final :func:`_validate_unique_names` pass. + """ + seen: set[str] = set() + for tool_config in tool_configs: + name = _declared_config_name(tool_config) + if name is not None: + _check_tool_name(name, seen) + + def _validate_unique_names(tool_specs: Sequence[ToolSpec]) -> None: seen: set[str] = set() for tool_spec in tool_specs: - # The harness registers custom tools by name beside its built-ins, so a same-named custom - # tool would silently replace the built-in the platform activates on every run. - if tool_spec.name.strip().lower() in PI_BUILTIN_TOOL_NAMES: - raise ReservedToolNameError(tool_spec.name) - if tool_spec.name in seen: - raise DuplicateToolNameError(tool_spec.name) - seen.add(tool_spec.name) + _check_tool_name(tool_spec.name, seen) class ToolResolver: @@ -118,6 +154,7 @@ def __init__( self._missing_secret_policy = missing_secret_policy async def resolve(self, tool_configs: Sequence[ToolConfig]) -> ResolvedToolSet: + _validate_declared_config_names(tool_configs) for tool_config in tool_configs: if isinstance(tool_config, BuiltinToolConfig): log.warning( diff --git a/sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py b/sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py index e9f3e25818..2d4afd5d0a 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py +++ b/sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py @@ -238,6 +238,62 @@ async def test_a_builtin_name_is_reserved_whatever_its_case(name): await ToolResolver().resolve([ClientToolConfig(name=name)]) +async def test_reserved_name_fails_fast_before_secret_lookup_and_adapter_calls(): + # A reserved-named ``code`` tool that also declares a missing secret must fail up front + # with ReservedToolNameError (not MissingToolSecretError), and neither the secret + # provider nor any adapter resolver may be invoked. + secrets = DictSecretProvider({}) + adapter_calls: list[str] = [] + + class RecordingGatewayResolver(FakeGatewayResolver): + async def resolve(self, tools): + adapter_calls.append("gateway") + return await super().resolve(tools) + + with pytest.raises(ReservedToolNameError): + await ToolResolver( + secret_provider=secrets, + gateway_resolver=RecordingGatewayResolver(), + ).resolve( + [ + CodeToolConfig(name="read", script="...", secrets=["TOKEN"]), + GatewayToolConfig( + integration="github", + action="GET_USER", + connection="c1", + ), + ] + ) + + assert secrets.requests == [] + assert adapter_calls == [] + + +async def test_reserved_gateway_name_fails_before_adapter_call(): + # A gateway tool whose declared name collides with a built-in is rejected before the + # adapter resolver runs, so no adapter work happens for a payload that will be refused. + adapter_calls: list[str] = [] + + class RecordingGatewayResolver(FakeGatewayResolver): + async def resolve(self, tools): + adapter_calls.append("gateway") + return await super().resolve(tools) + + with pytest.raises(ReservedToolNameError): + await ToolResolver(gateway_resolver=RecordingGatewayResolver()).resolve( + [ + GatewayToolConfig( + integration="github", + action="GET_USER", + connection="c1", + name="read", + ) + ] + ) + + assert adapter_calls == [] + + async def test_a_bare_tool_name_string_is_ignored_too(): # `coerce_tool_config` turns a bare string into a BuiltinToolConfig. resolved = await ToolResolver().resolve(coerce_tool_configs(["read"])) From 25a7bcad56070f0505a766a5a2459cd8688d9ade Mon Sep 17 00:00:00 2001 From: Marcelo Date: Sun, 2 Aug 2026 22:12:33 -0300 Subject: [PATCH 2/2] chore: trigger CLA re-evaluation after email verification