Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# ── CLI extensions (AI) ──────────────────────────────────────────────────────
/cli/azd/extensions/azure.ai.agents/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @huimiu @hund030
/cli/azd/test/functional/ai_agents/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @huimiu @hund030
/cli/azd/extensions/azure.ai.connections/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn @huimiu @hund030
/cli/azd/extensions/azure.ai.finetune/ @JeffreyCA @trangevi @achauhan-scc @kingernupur @saanikaguptamicrosoft
/cli/azd/extensions/azure.ai.foundry/ @JeffreyCA @glharper @trangevi @trrwilson @therealjohn
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint-ext-azure-ai-agents.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: ext-azure-ai-agents-ci
name: ext-azure-ai-agents-lint

on:
pull_request:
Expand All @@ -13,7 +13,7 @@ concurrency:

permissions:
contents: read
pull-requests: write
pull-requests: write # required by reusable workflow lint-go.yml

jobs:
lint:
Expand Down
84 changes: 84 additions & 0 deletions .github/workflows/test-ext-azure-ai-agents.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: ext-azure-ai-agents-test

on:
pull_request:
paths:
- "cli/azd/extensions/azure.ai.agents/**"
- "cli/azd/test/functional/ai_agents/**"
- ".github/workflows/test-ext-azure-ai-agents.yml"
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version-file: cli/azd/go.mod

- name: Build azd
working-directory: cli/azd
run: go build -tags=record -o azd-record ./

- name: Build extension (record-tagged)
working-directory: cli/azd/extensions/azure.ai.agents
run: go build -tags=record -o azure-ai-agents .

- name: Install extension
run: |
EXT_DIR="$HOME/.azd/extensions/azure.ai.agents"
mkdir -p "$EXT_DIR"
cp cli/azd/extensions/azure.ai.agents/azure-ai-agents "$EXT_DIR/azure-ai-agents-linux-amd64"
# Register extension in azd config
mkdir -p "$HOME/.azd"
python3 -c "
import json, pathlib
config = {
'extension': {
'installed': {
'azure.ai.agents': {
'id': 'azure.ai.agents',
'namespace': 'ai.agent',
'capabilities': ['custom-commands', 'lifecycle-events', 'mcp-server', 'service-target-provider', 'metadata'],
'displayName': 'Foundry agents (Preview)',
'description': 'Ship agents with Microsoft Foundry from your terminal. (Preview)',
'version': '0.0.0-test',
'usage': 'azd ai agent <command> [options]',
'path': 'extensions/azure.ai.agents/azure-ai-agents-linux-amd64',
'source': 'azd'
}
}
}
}
pathlib.Path.home().joinpath('.azd', 'config.json').write_text(json.dumps(config, indent=2))
"
cat "$HOME/.azd/config.json"

- name: Run Tier 0 tests (offline)
working-directory: cli/azd
env:
CLI_TEST_AZD_PATH: ${{ github.workspace }}/cli/azd/azd-record
run: |
go test -tags=record -run "Test_AIAgent_(Version|Help|Init_NoPrompt_MissingFlags|SampleList|Doctor_Help)" \
./test/functional/ai_agents/ -v -timeout 5m

- name: Run Tier 1 tests (playback)
working-directory: cli/azd
env:
AZURE_RECORD_MODE: playback
CLI_TEST_AZD_PATH: ${{ github.workspace }}/cli/azd/azd-record
run: |
# Make azd-record available as "azd" on PATH so that
# azidentity.AzureDeveloperCLICredential (exec.LookPath("azd")) works in playback
sudo ln -sf "$GITHUB_WORKSPACE/cli/azd/azd-record" /usr/local/bin/azd
go test -tags=record -run "Test_AIAgent_Init_(NoPrompt_(Defer|WithProject)|NegativeControl_BadCassette)" \
./test/functional/ai_agents/ -v -timeout 5m
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.ai.agents/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ words:
- deepseek
- ttfb
- Bhadauria
# Test infrastructure
- recordproxy
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package azure

import (
"azureaiagent/internal/pkg/recordproxy"
"azureaiagent/internal/version"
"fmt"
"net/http"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
Expand All @@ -17,7 +19,7 @@ import (
func NewArmClientOptions() *arm.ClientOptions {
userAgent := fmt.Sprintf("azd-ext-azure-ai-agents/%s", version.Version)

return &arm.ClientOptions{
opts := &arm.ClientOptions{
ClientOptions: policy.ClientOptions{
Logging: policy.LogOptions{
AllowedHeaders: []string{azsdk.MsCorrelationIdHeader},
Expand All @@ -28,4 +30,11 @@ func NewArmClientOptions() *arm.ClientOptions {
},
},
}

// In record/playback mode, inject proxy transport so ARM calls route through the recording proxy.
if recordproxy.Transport != nil {
opts.ClientOptions.Transport = &http.Client{Transport: recordproxy.Transport}
}

return opts
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//go:build !record

package recordproxy

import "net/http"

// Transport is nil in non-record builds — no proxy is used.
var Transport http.RoundTripper
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//go:build record

// Package recordproxy intercepts ALL outbound HTTP traffic when the "record"
// build tag is active, routing it through the recording proxy (azd-record).
// This includes http.DefaultTransport (covers http.Client{} and similar) and
// Azure SDK clients (via client_options.go). Third-party SDKs that accept a
// custom transport parameter should also use Transport from this package to
// ensure their traffic is captured during recording/playback.
package recordproxy

import (
"crypto/tls"
"net/http"
"net/url"
"os"
)

// Transport is the proxy-aware transport for record/playback mode.
// It routes all HTTP traffic through the recording proxy (AZD_TEST_HTTPS_PROXY).
// nil when the env var is not set.
var Transport http.RoundTripper

func init() {
val, ok := os.LookupEnv("AZD_TEST_HTTPS_PROXY")
if !ok {
return
}
proxyUrl, err := url.Parse(val)
if err != nil {
panic("recordproxy: invalid AZD_TEST_HTTPS_PROXY URL: " + err.Error())
}

base, ok := http.DefaultTransport.(*http.Transport)
if !ok {
panic("recordproxy: http.DefaultTransport is not *http.Transport")
}
transport := base.Clone()
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true, //nolint:gosec // recording proxy uses self-signed cert
}
transport.Proxy = http.ProxyURL(proxyUrl)

// This affects extension's own http.Client{} (init.go:937) which uses DefaultTransport.
// Azure SDK ARM clients are handled separately via ClientOptions.Transport in client_options.go.
http.DefaultTransport = transport
Transport = transport
}
8 changes: 5 additions & 3 deletions cli/azd/test/azdcli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,11 @@ func startTestCredentialServer(t *testing.T) *httptest.Server {
Token string `json:"token"`
ExpiresOn string `json:"expiresOn"`
}{
Status: "success",
Token: mockJWT,
ExpiresOn: expiresOn.Format(time.RFC3339),
Status: "success",
Token: mockJWT,
// Use explicit UTC Z-suffix format (not time.RFC3339 which may emit +00:00 offset).
// The extension parser only accepts the Z-terminated form.
ExpiresOn: expiresOn.UTC().Format("2006-01-02T15:04:05Z"),
}
Comment thread
v1212 marked this conversation as resolved.

w.Header().Set("Content-Type", "application/json")
Expand Down
Loading
Loading