Skip to content

Bug: resolve_tool_manifest_policy produces duplicate GetToolSpec causing Tool names must be unique API error #1564

Description

@1688mengdie

Summary

When GetToolSpec is explicitly included in allowed_tools and collapsed tools are present, resolve_tool_manifest_policy pushes GetToolSpec into expanded_tool_names twice, causing duplicate entries in the final tool_definitions sent to the AI provider. The provider rejects the request with:

HTTP 400: Tool names must be unique.

Steps to Reproduce

  1. Configure the agent with GetToolSpec explicitly in the tool allowlist
  2. Also include any tool whose default_exposure is Collapsed (e.g. WebFetch, WebSearch, Git, GetFileDiff)
  3. Send a request → AI provider returns Tool names must be unique

This is commonly hit when enabling a large set of tools (e.g. all ~60 available) because the conditions are easily met.

Root Cause

src/crates/execution/tool-contracts/src/framework.rs, function resolve_tool_manifest_policy:

  • Lines 240–253: iterates allowed_tools, pushes GetToolSpec into expanded_tool_names once (it has Expanded default exposure).
  • Lines 262–267: when collapsed tools exist, unconditionally pushes GetToolSpec into expanded_tool_names again, without a dedup check.

Note that allowed_tool_names already has a dedup guard (line 256–258), but expanded_tool_names does not — an asymmetric omission.

Known Workaround

Manually exclude GetToolSpec from the tool list. The system auto-adds it when collapsed tools are present.

Related Test Coverage (confirms the bug)

Two tests currently assert the duplicate as expected behavior:

Test File Current Expected
product_manifest_preserves_explicit_get_tool_spec_runtime_contract catalog.rs:706 ["WebFetch", "GetToolSpec", "GetToolSpec"]
tool_manifest_policy_preserves_explicit_get_tool_spec_duplicate_runtime_contract tool_contracts.rs:1569 [GET_TOOL_SPEC, GET_TOOL_SPEC]

Proposed Fix

Add dedup check before pushing to expanded_tool_names, mirroring the existing guard on allowed_tool_names:

         if tool_snapshot
             .iter()
             .any(|tool| tool.name == get_tool_spec_tool_name)
+            && !expanded_tool_names
+                .iter()
+                .any(|name| name == get_tool_spec_tool_name)
         {
             expanded_tool_names.push(get_tool_spec_tool_name.to_string());
         }

Tests updated: the two tests above should assert single occurrence instead of duplicate.

Verification:

  • cargo check --workspace — pass
  • cargo test -p bitfun-agent-tools — 143 pass
  • cargo test -p bitfun-core -- catalog — 22 pass
  • cargo test -p bitfun-agent-runtime --test post_call_hook_execution_contracts — 3 pass
  • pnpm run i18n:contract:test — 37 pass

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions