Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package cmd
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -1472,12 +1474,8 @@ func ensureProject(
fmt.Println("Let's get your project initialized.")

// Environment creation is handled separately in ensureEnvironment
initArgs := []string{
"init", "-t", "Azure-Samples/azd-ai-starter-basic", targetDir,
}
if flags.env != "" {
initArgs = append(initArgs, "--environment", flags.env)
} else {
envName := flags.env
if envName == "" {
// Derive environment name from target folder
envBase := targetDir
if targetDir == "." {
Expand All @@ -1490,8 +1488,12 @@ func ensureProject(
if len(base) > 59 {
base = strings.TrimRight(base[:59], "-")
}
envName := base + "-dev"
initArgs = append(initArgs, "--environment", envName)
envName = base + "-dev"
}

initArgs := []string{
"init", "-t", "Azure-Samples/azd-ai-starter-basic", targetDir,
"--environment", envName,
}

// We don't have a project yet
Expand All @@ -1518,6 +1520,11 @@ func ensureProject(
)
}

// Best-effort: generate a salt so uniqueString()-based resource names
// differ across project recreations. If anything fails the Bicep
// templates fall back to the original deterministic hash.
ensureResourceTokenSalt(ctx, azdClient, envName)

// Sync the extension process into the new project directory so that
// subsequent local file operations see the scaffolded project.
if targetDir != "." {
Expand Down Expand Up @@ -2673,6 +2680,47 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
return nil
}

//nolint:gosec // env var key name, not a credential
const resourceTokenSaltKey = "AZD_RESOURCE_TOKEN_SALT"

// ensureResourceTokenSalt checks whether the current azd environment already
// has a resource token salt. If not, it generates and stores one.
// Failures are silently ignored so the Bicep templates fall back to the
// original deterministic uniqueString() hash.
func ensureResourceTokenSalt(ctx context.Context, azdClient *azdext.AzdClient, envName string) {
// Already have a salt from a previous init — keep it so resource names stay stable.
existing, err := azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{
EnvName: envName,
Key: resourceTokenSaltKey,
})
if err == nil && existing.Value != "" {
return
}

// Generate a random salt; if entropy fails, fall back to deterministic naming.
salt, err := generateResourceTokenSalt()
if err != nil {
return
}

// Persist the salt into the azd environment; if storage fails, provision
// will still work with the original deterministic resource names.
_, _ = azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{
EnvName: envName,
Key: resourceTokenSaltKey,
Value: salt,
})
}

// generateResourceTokenSalt returns a random 8-character hex string.
func generateResourceTokenSalt() (string, error) {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}

// resolveCollisions checks whether the auto-computed target directory or
// service name already exist. When a collision is detected, the user is
// prompted for a new name (or a numeric suffix is appended in no-prompt
Expand Down
24 changes: 24 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cmd

import (
"context"
"encoding/hex"
"errors"
"io/fs"
"net/http"
Expand All @@ -20,6 +21,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -2939,3 +2941,25 @@ func TestAbsolutizeRelativeManifestPaths_SrcEscapeRegression(t *testing.T) {
t.Errorf("src must remain relative %q to stay inside project, got %q", "src", flags.src)
}
}

func TestGenerateResourceTokenSalt(t *testing.T) {
salt, err := generateResourceTokenSalt()
require.NoError(t, err)

// Should be 8-character hex string (4 random bytes = 8 hex chars)
require.Len(t, salt, 8)

// Should be valid hex
_, err = hex.DecodeString(salt)
require.NoError(t, err)
}

func TestGenerateResourceTokenSalt_Unique(t *testing.T) {
seen := make(map[string]bool)
for range 100 {
salt, err := generateResourceTokenSalt()
require.NoError(t, err)
require.False(t, seen[salt], "duplicate salt generated: %s", salt)
seen[salt] = true
}
}
Loading