Skip to content

fix: emit provider-safe host tool names - #511

Merged
chubes4 merged 1 commit into
Automattic:mainfrom
linchpin:fix/provider-safe-host-tool-names
Aug 22, 2026
Merged

fix: emit provider-safe host tool names#511
chubes4 merged 1 commit into
Automattic:mainfrom
linchpin:fix/provider-safe-host-tool-names

Conversation

@aaronware

Copy link
Copy Markdown
Contributor

Summary

  • emit provider_safe_name for host tool declarations in WP_Agent_Default_Provider_Turn_Adapter::function_declarations()
  • update the default provider-turn adapter smoke coverage, including three assertions whose expectation changes

Why

Host tool declarations reach the provider under their canonical namespaced name, and provider tool-name validation rejects the /. This is the same constraint #320 fixed for client runtime declarations — on the half #320 did not cover.

Client runtime tools could sidestep it, because the consumer picks the name and can simply declare filesystem_write. Host tools cannot: they are derived from abilities, and the Abilities API requires a namespaced namespace/name and refuses to register anything else. So every ability-backed declaration carries a /.

Observed against a live OpenAI-compatible endpoint, using the shipped default chat handler with an ability-backed toolset:

tools.0.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$'

The same request succeeds with this patch applied, executes the ability, and reports the canonical name back in tool_observability.

For what it is worth, this is not specific to one connector: FunctionDeclaration stores the name verbatim, AbstractOpenAiCompatibleTextGenerationModel::prepareToolsParam() emits it verbatim, and the official ai-provider-for-anthropic connector does the same ('name' => $functionDeclaration->getName()). Nothing between the declaration and the wire sanitizes it.

What was already in place

Only the outbound mapping was missing:

  • normalizeForConversationRequest() already records provider_safe_name whenever the canonical name is not provider-safe
  • WP_Agent_Provider_Turn_Request normalizes declarations on construction, so the alias is present by the time the adapter maps them
  • WP_Agent_Tool_Execution_Core already resolves the provider's emitted name back via canonicalNameForProviderToolName(), so mediation still matches the canonical declaration

Review note

Three existing assertions change expectation rather than being added to, and they are the ones worth a close look:

Assertion Was Now
function declaration carries the … tool name client/lookup client__lookup
dispatch payload function declaration carries the … tool name client/lookup client__lookup
real-schema parameter assertions keyed client/lookup keyed client__lookup

They asserted that the canonical namespaced name is what reaches the provider, which is the behaviour this corrects. If that was load-bearing intent rather than recorded behaviour, this PR is the wrong shape and I would rather know.

The suite could not have caught this on its own: every provider result in it is stubbed via $make_result(), so no test sends a tool name to anything that validates one. Added assertions now check every emitted name against ^[a-zA-Z0-9_-]{1,128}$.

Testing

  • composer phpstan — no errors
  • composer smoke — all suites pass (default-provider-turn-adapter-smoke.php: 116 assertions)
  • verified end to end against a live OpenAI-compatible provider with an ability-backed toolset, before and after

AI assistance

  • AI assistance: Yes
  • Tool(s): Claude Code (Opus 5)
  • Used for: Root-cause investigation, the change, smoke coverage, and local verification. Aaron Ware remains responsible for the contents of this PR.

Host tool declarations reach the provider under their canonical namespaced
name, and provider tool-name validation rejects the `/`. This is the same
constraint Automattic#320 addressed for client runtime declarations, on the half that
Automattic#320 did not cover.

Client runtime tools could sidestep it, because the consumer chooses the name
and can simply declare `filesystem_write`. Host tools cannot: they are derived
from abilities, and the Abilities API requires a namespaced `namespace/name`,
refusing to register anything else. So every ability-backed declaration carries
a `/`, and `WP_Agent_Default_Provider_Turn_Adapter` emits it verbatim.

Observed against a live OpenAI-compatible endpoint, using the shipped default
chat handler with an ability-backed toolset:

    tools.0.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$'

The alias this needs is already computed and already carried:
`normalizeForConversationRequest()` records `provider_safe_name` on any
declaration whose canonical name is not provider-safe, and
`WP_Agent_Provider_Turn_Request` normalizes declarations on construction, so it
is present by the time the adapter maps them. The return leg is already wired
too — `WP_Agent_Tool_Execution_Core` resolves the provider's emitted name back
through `canonicalNameForProviderToolName()`, so mediation still matches the
canonical declaration and tool observability still reports the canonical name.

Only the outbound mapping was missing.

Three existing assertions changed expectation rather than being added to, and
they are the ones worth reviewing: they asserted that the canonical namespaced
name is what reaches the provider, which is the behaviour being corrected. The
suite could not have caught this otherwise, because every provider result in it
is stubbed, so no test sends a tool name to anything that validates it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chubes4

chubes4 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

@aaronware thanks for the contribution! looks good to me

@chubes4
chubes4 merged commit f0457e0 into Automattic:main Aug 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants