Skip to content

feat(agents): add azd ai agent toolbox direct commands#8203

Open
hund030 wants to merge 4 commits into
mainfrom
zhihuan/feat-agent-toolbox-impl
Open

feat(agents): add azd ai agent toolbox direct commands#8203
hund030 wants to merge 4 commits into
mainfrom
zhihuan/feat-agent-toolbox-impl

Conversation

@hund030
Copy link
Copy Markdown
Collaborator

@hund030 hund030 commented May 15, 2026

Summary

Adds the azd ai agent toolbox command group to the azure.ai.agents extension so users can manage versioned, connection-backed tool collections without leaving their terminal. Closes #8143. Implements the design spec from #8160.

Changes

  • internal/cmd/toolbox*.go: six verbs (create, update, delete, show, list, connection {add,remove,list}) and the parent command. create records a local pending entry; the first connection add POSTs v1 and clears it. connection add resolves RemoteTool connections to mcp tool entries and CognitiveSearch (with --index) to azure_ai_search entries; later mutations fetch the default version, edit tools[], POST a new version, then PATCH default_version. Reuses the central resolveProjectEndpoint cascade and the ResolvedEndpoint source enum.
  • internal/cmd/pending_toolboxes.go: per-endpoint pending-toolbox store under extensions.ai-agents.pending-toolboxes.<sha256(endpoint)[:16]> (§ 7); exposed via a pendingToolboxStore interface so tests can substitute.
  • internal/pkg/azure/foundry_toolsets_client.go: extended with ListToolboxes, GetToolboxVersion, ListToolboxVersions, DeleteToolboxVersion, SetDefaultVersion, capped paginator, and Endpoint(). Pipeline helpers (doJSON, toolboxURL, generic listPagedFromClient[T]) collapse the per-method boilerplate.
  • internal/pkg/azure/foundry_projects_client.go: adds GetConnection(ctx, name) (no credentials) for the connection resolver.
  • internal/exterrors/codes.go: new codes (CodeToolboxNotFound, CodeDefaultVersionDelete, CodeOnlyVersionDelete, CodeUnsupportedConnectionCategory, CodeMissingIndex, CodeUnsupportedIndexFlag, CodeDuplicateConnection, CodeConnectionNotFound, CodeConnectionMissingTarget, CodeConnectionNotInToolbox, CodeLastToolRemoval, CodeMissingForceFlag, CodeInvalidToolboxName, CodeMissingUpdateField, CodePendingToolboxStoreFailed, CodeLastToolRemoval) and Op* constants for ServiceFromAzure.
  • Unit tests: table-driven coverage for every verb's branches against a stubbed toolboxClient and connectionResolver, plus the shared helpers (buildToolEntry, filterOutConnection, duplicateConnectionInTools, buildToolboxMcpURL, endpointBucketKey).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

📋 Prioritization Note

Thanks for the contribution! The linked issue isn't in the current milestone yet.
Review may take a bit longer — reach out to @rajeshkamal5050 or @kristenwomack if you'd like to discuss prioritization.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new azd ai agent toolbox command group to the azure.ai.agents extension to manage Foundry toolboxes as versioned, connection-backed tool collections, including a local “pending toolbox” flow for create prior to the first connection add.

Changes:

  • Introduces toolbox CRUD-ish verbs (create, update, delete, show, list) plus toolbox connection add|remove|list under the agent extension command tree.
  • Adds a per-endpoint pending-toolbox config store (bucketed by a hashed endpoint) and wires it into create, show, list, and promotion on connection add.
  • Extends the Foundry toolbox/projects Azure clients with toolbox pagination/version operations and a connection lookup without credentials, plus unit tests for the new command branches and helper functions.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_toolsets_client.go Adds shared JSON request helper, toolbox URL builder, cursor-pagination walker, and new toolbox/version operations.
cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go Adds GetConnection (no credentials) for toolbox connection resolution.
cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go Adds toolbox-specific error codes and Azure service operation names for toolbox flows.
cli/azd/extensions/azure.ai.agents/internal/cmd/root.go Registers the new toolbox command group in the extension root command.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox.go Adds toolbox parent command, cross-cutting flags, name/output validation, endpoint resolution, and 404 detection helper.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_client.go Defines a toolboxClient interface to allow unit tests to stub the toolbox API.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_shared.go Adds shared helpers for JSON emission, toolbox-not-found mapping, and tool connection ID walking.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_context.go Adds toolbox/projects client constructors and endpoint parsing helpers.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_create.go Implements toolbox create as a local pending record (no initial POST).
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_update.go Implements toolbox update (PATCH default version only).
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_delete.go Implements toolbox delete including guarded per-version delete semantics and pending-record clearing.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_show.go Implements toolbox show, including MCP endpoint computation and pending-record rendering.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_list.go Implements toolbox list, merging live toolboxes with local pending entries.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection.go Adds toolbox connection add and tool-entry construction logic based on connection category.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection_actions.go Implements toolbox connection remove and toolbox connection list.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection_resolver.go Adds a resolver that fetches a project connection (no credentials) and maps it into toolbox-ready shape.
cli/azd/extensions/azure.ai.agents/internal/cmd/pending_toolboxes.go Implements per-endpoint pending-toolbox storage and the pendingToolboxStore abstraction.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_test_helpers_test.go Adds mock toolbox client, stub connection resolver, and in-memory pending store for tests.
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_helpers_test.go Adds unit tests for helper utilities (validation, tool entry building, filtering, URL building, hashing).
cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_commands_test.go Adds unit tests covering command branch behavior and key error cases.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox.go
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_list.go Outdated
Comment thread cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_toolsets_client.go Outdated
'connection add' against the same toolbox name publishes v1 and clears the
pending record.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return an error with suggestion so its clear what to specify. All it says now is:

ERROR: accepts 1 arg(s), received 0

I'd expect it show more like the --help comamnd does:

Usage:
  agent toolbox create <name> [flags]

Foundry requires a non-empty tool list on the first POST, so 'create' does not
contact the service. Instead it records a local pending entry. The first
'connection add' against the same toolbox name publishes v1 and clears the
pending record.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be outside of extension control, I think that's coming from Args: cobra.ExactArgs(1) being specified

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The error comes from cobra.ExactArgs(1), but we can customize the args func if we want to customize the error message.
I suppose to file a follow-up issue since the scope is the whole azd ai agent surface, not specific to toolbox.

}
fmt.Printf(
"Registered toolbox %s (pending tools). "+
"Run 'azd ai agent toolbox connection add %s <connection>' to publish v1.\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve the formatting for this so its not all on one line - there might be an existing pattern for showing next step guidance and coloring, etc.

If not, then just make each sentence on a new line.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the actions.ActionResult structure for core-azd commands, but it doesn't apply to our extension. Extensions are cobra commands that write directly to stdout.

I will make each sentence on a new line.


cmd.Flags().StringVar(
&flags.index, "index", "",
"Index name (required when the connection's category is CognitiveSearch).",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is CognitiveSearch referring to? Need to check if that is the correct branding/service name now.

Copy link
Copy Markdown
Collaborator Author

@hund030 hund030 May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"CognitiveSearch" is the literal string the Foundry service returns as the connection type field.

It's also used by the agent YAML manifest schema:

CategoryCognitiveSearch CategoryKind = "CognitiveSearch"

Updated help text: "CognitiveSearch is the category for Azure AI Search"

return nil, exterrors.Validation(
exterrors.CodeUnsupportedConnectionCategory,
fmt.Sprintf(
"connection %q has category %q; v1 supports RemoteTool and CognitiveSearch only",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect this limitation.

Copy link
Copy Markdown
Collaborator Author

@hund030 hund030 May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full list of project connection categories is defined in

const (
ConnectionTypeAzureOpenAI ConnectionType = "AzureOpenAI"
ConnectionTypeAzureBlob ConnectionType = "AzureBlob"
ConnectionTypeAzureStorageAccount ConnectionType = "AzureStorageAccount"
ConnectionTypeCognitiveSearch ConnectionType = "CognitiveSearch"
ConnectionTypeContainerRegistry ConnectionType = "ContainerRegistry"
ConnectionTypeCosmosDB ConnectionType = "CosmosDB"
ConnectionTypeApiKey ConnectionType = "ApiKey"
ConnectionTypeAppConfig ConnectionType = "AppConfig"
ConnectionTypeAppInsights ConnectionType = "AppInsights"
ConnectionTypeCustomKeys ConnectionType = "CustomKeys"
ConnectionTypeRemoteTool ConnectionType = "RemoteTool"
)

The other categories don't have a corresponding tool shape in the Foundry service.
Could you help clarify with service team?

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection.go Outdated
'connection add' against the same toolbox name publishes v1 and clears the
pending record.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might be outside of extension control, I think that's coming from Args: cobra.ExactArgs(1) being specified

CognitiveSearch → azure_ai_search tool (requires --index)
Other categories are rejected.

If the toolbox has a local pending record (from 'toolbox create'), v1 is
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that if I want to create a toolbox with 3 tools, I'm going to end up on v3 before I'm done. This feels incredibly painful from a user perspective, as I'm not actually iterating on desired changes, I'm being forced into it. Is there a way this can be done without all these added, unwanted, versions?

if _, err := store.Clear(ctx, endpoint, toolboxName); err != nil {
return exterrors.Internal(
exterrors.CodePendingToolboxStoreFailed,
fmt.Sprintf("failed to clear pending toolbox record: %s", err),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the implication here? Is this something the user can recover from? At the very least we should probably indicate that this is a client side only failure, and that the toolbox version was still created correctly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Now the user keeps the success message, the stale local record is either silently reconciled by the next list or cleaned up by delete, and the underlying error is preserved in the log stream for --debug triage.

return exterrors.ServiceFromAzure(err, exterrors.OpGetToolbox)
}

current, err := client.GetToolboxVersion(ctx, toolboxName, tb.DefaultVersion)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a way to add a connection to the non-default toolbox version?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in v1. We can have a --version flag on connection add if you feel necessary. Add @therealjohn for confirm.

@@ -0,0 +1,281 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that the add command is in one file, and the other commands are in a different file? I'd suggest either a separate file per command, like the toolbox base commands, or everything in one. Could separate out the helper methods if desired to help clean things up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. One file per command now.

return exterrors.Validation(
exterrors.CodeMissingForceFlag,
"--no-prompt requires --force on destructive operations",
"add --force to confirm the deletion non-interactively",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this only apply if we're trying to delete the default?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the guard was over-broad.

The noPrompt && !force check moved out of the shared path and now lives only in the whole-toolbox delete path.

}

// Drop pending records that already exist live-side to avoid duplicates.
liveNames := map[string]struct{}{}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the toolbox has been published to the project, we shouldn't have a pending entry for it any more, correct? Is this necessary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The pending record is normally cleared when connection add publishes, but a clear failure can leave a stale entry. This dedup makes the list output self-healing.

extCtx = ensureExtensionContext(extCtx)
cmd := &cobra.Command{
Use: "list",
Short: "List toolboxes on the project, plus any local pending records.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this list all versions, or just the defaults?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the defaults. One row per toolbox, showing default_version.

Short: "Show a toolbox version, including its computed MCP endpoint.",
Long: `Show a toolbox.

By default shows the default version. Use --version to inspect a specific
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can a user know how many versions a toolbox has?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data-plane endpoint exists but no azd ai agent toolbox version list verb is exposed. We can have add it if you feel necessary. Also add @therealjohn to confirm.

Long: `Update a toolbox.

Only --default-version is mutable through PATCH today (§ 4.1). To edit the
description or the tool list, publish a new version with 'connection add' or
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connection add doesn't let a user change the description, does it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. Thanks for the catch. Fixed the help text to drop the description-editing claim.

Copy link
Copy Markdown
Member

@jongio jongio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical findings from code-level analysis. I'm not restating the design/UX feedback from @therealjohn and @trangevi; their reviews cover the broader picture.

Three findings, one medium priority:

  1. [MED] Non-atomic version promotion connection add and connection remove both call CreateToolboxVersion then SetDefaultVersion as two separate API calls. If the second fails, there's an orphaned version that isn't the default, and the error doesn't include the created version number, so recovery via toolbox update --default-version requires the user to first figure out which version got created. Same pattern in toolbox_connection_actions.go around line 120.

  2. [LOW] Silent pagination truncation listPagedFromClient returns partial results without error when the server responds with has_more=true but provides no usable cursor (lines 161-164 of foundry_toolsets_client.go). Unlikely in practice, but a log.Printf warning here would make debugging much easier if it ever happens.

  3. [LOW] No length cap on toolbox/tool names toolboxNamePattern is ^[A-Za-z0-9_-]+$ with no min/max bounds. The existing agent name validation in parse.go enforces 1-63 chars. Worth adding a length cap so users get a clear local error rather than a less helpful service-side 400.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox_connection.go Outdated
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/toolbox.go
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.

Add azd ai toolbox create/update/show/list/delete (plus connection and tag subcommands) to manage Foundry Toolboxes from any directory

5 participants