From bbf9e11af080a211846e7b9ef005533e911a89a4 Mon Sep 17 00:00:00 2001 From: huimiu Date: Wed, 15 Jul 2026 11:12:41 +0800 Subject: [PATCH 1/2] fix: resolve toolbox $ref config before deploy --- .../extensions/azure.ai.toolboxes/README.md | 1 - .../internal/cmd/service_target.go | 91 ++++++++++++++++++- .../internal/cmd/service_target_test.go | 35 +++++++ 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/cli/azd/extensions/azure.ai.toolboxes/README.md b/cli/azd/extensions/azure.ai.toolboxes/README.md index 406294ec0ee..4b395d43c90 100644 --- a/cli/azd/extensions/azure.ai.toolboxes/README.md +++ b/cli/azd/extensions/azure.ai.toolboxes/README.md @@ -22,4 +22,3 @@ Get the endpoint value from `azd ai toolbox show ` (the `Endpoint:` line). The value may contain `${VAR}` references, which resolve against the azd environment. Because a toolbox version is immutable, `endpoint` cannot be combined with `tools` or `description`. - diff --git a/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target.go b/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target.go index d97321acaeb..4542c761052 100644 --- a/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target.go +++ b/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "strings" "azure.ai.toolboxes/internal/exterrors" @@ -15,6 +16,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/foundry" + "google.golang.org/protobuf/types/known/structpb" ) // aiToolboxHost is the azure.yaml service host kind owned by this extension. A @@ -126,7 +128,10 @@ func (p *toolboxServiceTarget) Deploy( targetResource *azdext.TargetResource, progress azdext.ProgressReporter, ) (*azdext.ServiceDeployResult, error) { - cfg, err := parseToolboxServiceConfig(serviceConfig) + cfg, err := p.parseToolboxServiceConfig( + ctx, + serviceConfig, + ) if err != nil { return nil, err } @@ -305,15 +310,62 @@ func (p *toolboxServiceTarget) buildToolEntries( // back to the deprecated config: shape for azure.yaml files written before the // per-resource service split. func parseToolboxServiceConfig(svc *azdext.ServiceConfig) (*toolboxServiceConfig, error) { - props := svc.GetAdditionalProperties() - if props == nil || len(props.GetFields()) == 0 { - props = svc.GetConfig() + return parseToolboxServiceConfigAtRoot(svc, "") +} + +func (p *toolboxServiceTarget) parseToolboxServiceConfig( + ctx context.Context, + svc *azdext.ServiceConfig, +) (*toolboxServiceConfig, error) { + props := toolboxServiceProps(svc) + if props == nil || !containsToolboxFileRef(props.AsMap()) { + return parseToolboxServiceConfig(svc) } + project, err := p.azdClient.Project().Get( + ctx, + &azdext.EmptyRequest{}, + ) + if err != nil { + return nil, fmt.Errorf( + "resolving project root for toolbox %q: %w", + svc.GetName(), + err, + ) + } + if project.GetProject() == nil { + return nil, fmt.Errorf( + "resolving project root for toolbox %q: empty project", + svc.GetName(), + ) + } + return parseToolboxServiceConfigAtRoot( + svc, + project.GetProject().GetPath(), + ) +} + +func parseToolboxServiceConfigAtRoot( + svc *azdext.ServiceConfig, + projectRoot string, +) (*toolboxServiceConfig, error) { + props := toolboxServiceProps(svc) cfg := &toolboxServiceConfig{} if props == nil { return cfg, nil } - b, err := json.Marshal(props.AsMap()) + values := props.AsMap() + if projectRoot != "" { + resolved, err := foundry.ResolveFileRefs(values, projectRoot) + if err != nil { + return nil, fmt.Errorf( + "resolving toolbox service %q config: %w", + svc.GetName(), + err, + ) + } + values = resolved + } + b, err := json.Marshal(values) if err != nil { return nil, fmt.Errorf("encoding toolbox service %q config: %w", svc.GetName(), err) } @@ -323,6 +375,35 @@ func parseToolboxServiceConfig(svc *azdext.ServiceConfig) (*toolboxServiceConfig return cfg, nil } +func toolboxServiceProps( + svc *azdext.ServiceConfig, +) *structpb.Struct { + props := svc.GetAdditionalProperties() + if props == nil || len(props.GetFields()) == 0 { + return svc.GetConfig() + } + return props +} + +func containsToolboxFileRef(value any) bool { + switch typed := value.(type) { + case map[string]any: + if _, exists := typed["$ref"]; exists { + return true + } + for _, child := range typed { + if containsToolboxFileRef(child) { + return true + } + } + case []any: + if slices.ContainsFunc(typed, containsToolboxFileRef) { + return true + } + } + return false +} + // currentEnvValues loads all key-value pairs from the active azd environment, used to // resolve ${VAR} references in tool fields at deploy time. func (p *toolboxServiceTarget) currentEnvValues(ctx context.Context) (map[string]string, error) { diff --git a/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target_test.go b/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target_test.go index 8fece33c91a..2e097aed583 100644 --- a/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target_test.go +++ b/cli/azd/extensions/azure.ai.toolboxes/internal/cmd/service_target_test.go @@ -5,6 +5,8 @@ package cmd import ( "context" + "os" + "path/filepath" "testing" "github.com/azure/azure-dev/cli/azd/pkg/azdext" @@ -50,6 +52,39 @@ func TestParseToolboxServiceConfig_ServiceLevel(t *testing.T) { assert.Equal(t, "github-mcp", cfg.Tools[1]["connection"]) } +func TestParseToolboxServiceConfig_FileRef(t *testing.T) { + t.Parallel() + + root := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(root, "toolbox.yaml"), + []byte( + "description: referenced tools\n"+ + "tools:\n"+ + " - type: web_search\n", + ), + 0o600, + )) + props, err := structpb.NewStruct(map[string]any{ + "$ref": "./toolbox.yaml", + }) + require.NoError(t, err) + + cfg, err := parseToolboxServiceConfigAtRoot( + &azdext.ServiceConfig{ + Name: "referenced", + Host: aiToolboxHost, + AdditionalProperties: props, + }, + root, + ) + + require.NoError(t, err) + assert.Equal(t, "referenced tools", cfg.Description) + require.Len(t, cfg.Tools, 1) + assert.Equal(t, "web_search", cfg.Tools[0]["type"]) +} + func TestParseToolboxServiceConfig_Endpoint(t *testing.T) { t.Parallel() From ba1d6bec78d0ed351729c70d1799a90f20b71d78 Mon Sep 17 00:00:00 2001 From: huimiu Date: Wed, 15 Jul 2026 11:28:52 +0800 Subject: [PATCH 2/2] fix: restore toolbox README trailing newline --- cli/azd/extensions/azure.ai.toolboxes/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/extensions/azure.ai.toolboxes/README.md b/cli/azd/extensions/azure.ai.toolboxes/README.md index 4b395d43c90..406294ec0ee 100644 --- a/cli/azd/extensions/azure.ai.toolboxes/README.md +++ b/cli/azd/extensions/azure.ai.toolboxes/README.md @@ -22,3 +22,4 @@ Get the endpoint value from `azd ai toolbox show ` (the `Endpoint:` line). The value may contain `${VAR}` references, which resolve against the azd environment. Because a toolbox version is immutable, `endpoint` cannot be combined with `tools` or `description`. +