mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
dc4bafbc1e
* .NET: Add Hosted-AgentSkills sample for Foundry Skills integration Add a new hosted agent sample that demonstrates how to load behavioral guidelines from Foundry Skills at startup using AgentSkillsProvider and the progressive disclosure pattern (advertise -> load on demand). The sample: - Downloads SKILL.md files from Foundry via ProjectAgentSkills SDK - Extracts ZIP archives with zip-slip protection - Wires skills into AgentSkillsProvider as an AIContextProvider - Hosts the agent via the Responses protocol Ships two Contoso Outdoors skills matching the Python sample (PR #5822): - support-style: tone, formatting, signature guidelines - escalation-policy: when and how to escalate tickets Includes convenience provisioning gated behind PROVISION_SAMPLE_SKILLS env var, clearly documented as NOT a production pattern. Closes #5776 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Add unit tests and integration test for Hosted-AgentSkills Unit tests (14 tests, all passing): - ZIP extraction with zip-slip guard (valid archive, traversal attack, sibling-prefix attack, directory entries) - Skill name validation (rejects dots, separators, traversal patterns) - AgentSkillsProvider with downloaded skills (advertises both skills, load_skill returns canary tokens, unknown skill returns error) Container integration test: - New 'agent-skills' scenario in the test container that creates Contoso Outdoors skills on disk and wires AgentSkillsProvider - AgentSkillsHostedAgentFixture + 4 integration tests verifying: - Routine questions load support-style skill (STYLE-CANARY-3318) - Escalation triggers load escalation-policy (ESC-CANARY-7742) - Skills are advertised in system prompt - load_skill tool is invoked via FunctionCallContent Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Add smoke test, bootstrap, and docs for agent-skills integration - Add scripts/smoke.ps1 for local Docker smoke testing: builds the contributor image, runs the container, verifies both skills are loaded via canary tokens (STYLE-CANARY-3318, ESC-CANARY-7742) - Add 'agent-skills' to the bootstrap script scenario list - Add agent-skills row to the integration test README scenarios table - Exclude HostedAgentSkillsPatternTests from net472 (uses net8.0+ APIs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Update commented-out package versions to latest across all hosted samples Update the end-user PackageReference versions (in the commented-out sections) from 1.0.0 to the current latest NuGet versions: - Microsoft.Agents.AI: 1.6.1 - Microsoft.Agents.AI.Foundry: 1.6.1-preview.260514.1 - Microsoft.Agents.AI.Foundry.Hosting: 1.6.1-preview.260514.1 - Microsoft.Agents.AI.Hosting: 1.6.1-preview.260514.1 - Microsoft.Agents.AI.OpenAI: 1.6.1 - Microsoft.Agents.AI.Workflows: 1.6.1 Also adds explicit versions to Hosted-Workflow-Handoff which had bare PackageReference entries without Version attributes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Fix broken markdown links in Hosted-AgentSkills README Remove references to non-existent ../../README.md. Replace with inline instructions matching other hosted samples that don't have a parent README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Use OS-appropriate string comparison in zip-slip guard Use Ordinal on Unix (case-sensitive FS) and OrdinalIgnoreCase on Windows to prevent case-based path bypass on Linux containers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
173 lines
6.9 KiB
PowerShell
173 lines
6.9 KiB
PowerShell
#requires -Version 7.0
|
|
<#
|
|
.SYNOPSIS
|
|
One-time bootstrap of stable hosted agents for the Foundry.Hosting.IntegrationTests suite.
|
|
|
|
.DESCRIPTION
|
|
The IT fixture targets stable, scenario-keyed agent names (e.g. it-happy-path) and only
|
|
manages versions on each test run. The agent itself must already exist AND its managed
|
|
identity must hold the Azure AI User role on the project scope, otherwise inbound
|
|
inference calls fail with HTTP 500 PermissionDenied.
|
|
|
|
This script idempotently creates each scenario agent (with a placeholder version) and
|
|
grants Azure AI User on the project to its managed identity. Re-run it safely; existing
|
|
agents and role assignments are left in place.
|
|
|
|
.PARAMETER ProjectEndpoint
|
|
Foundry project endpoint, e.g. https://<account>.services.ai.azure.com/api/projects/<project>
|
|
|
|
.PARAMETER Image
|
|
Container image reference for the placeholder version (e.g. <acr>.azurecr.io/foundry-hosting-it:<tag>).
|
|
Use the value emitted by scripts/it-build-image.ps1.
|
|
|
|
.NOTES
|
|
Per-scenario data-plane RBAC (e.g. `Search Index Data Reader` on the Azure AI Search service
|
|
for the `azure-search-rag` scenario) is intentionally NOT performed by this script. Search,
|
|
Cosmos, and other backing services are treated as pre-existing infrastructure. Grant the
|
|
scenario-specific data role to the agent's managed identity manually after the first run
|
|
(see dotnet/tests/Foundry.Hosting.IntegrationTests/README.md).
|
|
|
|
.EXAMPLE
|
|
./it-bootstrap-agents.ps1 `
|
|
-ProjectEndpoint "https://my-acct.services.ai.azure.com/api/projects/my-proj" `
|
|
-Image "myacr.azurecr.io/foundry-hosting-it:abc123"
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)] [string] $ProjectEndpoint,
|
|
[Parameter(Mandatory)] [string] $Image
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$Scenarios = @(
|
|
'happy-path',
|
|
'tool-calling',
|
|
'tool-calling-approval',
|
|
'mcp-toolbox',
|
|
'custom-storage',
|
|
'memory',
|
|
'azure-search-rag',
|
|
'session-files',
|
|
'agent-skills'
|
|
)
|
|
|
|
# Resolve project ARM scope from the endpoint.
|
|
$endpointUri = [Uri]$ProjectEndpoint
|
|
$accountName = $endpointUri.Host.Split('.')[0]
|
|
$projectName = ($endpointUri.AbsolutePath.TrimEnd('/') -split '/')[-1]
|
|
$accountInfo = az cognitiveservices account list --query "[?name=='$accountName'].{name:name, rg:resourceGroup, sub:id}" | ConvertFrom-Json
|
|
if (-not $accountInfo) { throw "Could not find Cognitive Services account '$accountName'." }
|
|
$rg = $accountInfo[0].rg
|
|
$sub = ($accountInfo[0].sub -split '/')[2]
|
|
$projectScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$accountName/projects/$projectName"
|
|
Write-Host "Project scope: $projectScope"
|
|
|
|
$tok = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv
|
|
$headers = @{
|
|
Authorization = "Bearer $tok"
|
|
'Foundry-Features' = 'HostedAgents=V1Preview'
|
|
'Content-Type' = 'application/json'
|
|
}
|
|
|
|
foreach ($scenario in $Scenarios) {
|
|
$agentName = "it-$scenario"
|
|
Write-Host ""
|
|
Write-Host "=== $agentName ==="
|
|
|
|
# 1. Ensure the agent exists. Create a placeholder version if it doesn't.
|
|
$agent = $null
|
|
try {
|
|
$agent = Invoke-RestMethod -Method GET -Headers $headers `
|
|
-Uri "$ProjectEndpoint/agents/$agentName`?api-version=v1"
|
|
Write-Host " agent exists"
|
|
} catch {
|
|
if ($_.Exception.Response.StatusCode -ne 404) { throw }
|
|
}
|
|
|
|
if (-not $agent) {
|
|
Write-Host " creating placeholder version..."
|
|
$body = @{
|
|
definition = @{
|
|
kind = 'hosted'
|
|
container_protocol_versions = @(@{ protocol = 'responses'; version = '1.0.0' })
|
|
cpu = '0.25'
|
|
memory = '0.5Gi'
|
|
environment_variables = @{ IT_SCENARIO = $scenario }
|
|
image = $Image
|
|
}
|
|
metadata = @{ enableVnextExperience = 'true' }
|
|
} | ConvertTo-Json -Depth 10
|
|
Invoke-RestMethod -Method POST -Headers $headers `
|
|
-Uri "$ProjectEndpoint/agents/$agentName/versions`?api-version=v1" `
|
|
-Body $body | Out-Null
|
|
Start-Sleep 5
|
|
$agent = Invoke-RestMethod -Method GET -Headers $headers `
|
|
-Uri "$ProjectEndpoint/agents/$agentName`?api-version=v1"
|
|
}
|
|
|
|
$principalId = $agent.versions.latest.instance_identity.principal_id
|
|
Write-Host " agent MI: $principalId"
|
|
|
|
# 2. PATCH the agent endpoint to route via @latest if not already configured.
|
|
# Using @latest means each new version added by the IT fixture automatically becomes the
|
|
# served version, no per-run PATCH needed (which is good because the strongly-typed
|
|
# PATCH wrapper is alpha-only on Azure.AI.Projects right now).
|
|
$hasLatestSelector = $agent.agent_endpoint -and `
|
|
($agent.agent_endpoint.version_selector.version_selection_rules | Where-Object { $_.agent_version -eq '@latest' })
|
|
if ($hasLatestSelector) {
|
|
Write-Host " endpoint already routes via @latest"
|
|
} else {
|
|
Write-Host " patching endpoint to route via @latest..."
|
|
$patchBody = @{
|
|
agent_endpoint = @{
|
|
version_selector = @{
|
|
version_selection_rules = @(@{
|
|
type = 'FixedRatio'
|
|
agent_version = '@latest'
|
|
traffic_percentage = 100
|
|
})
|
|
}
|
|
protocols = @('responses')
|
|
}
|
|
} | ConvertTo-Json -Depth 10
|
|
Invoke-RestMethod -Method PATCH -Headers $headers `
|
|
-Uri "$ProjectEndpoint/agents/$agentName`?api-version=v1" `
|
|
-Body $patchBody | Out-Null
|
|
}
|
|
|
|
# 3. Grant Azure AI User on the project scope to the agent MI (idempotent).
|
|
$existing = az role assignment list --assignee $principalId --scope $projectScope `
|
|
--query "[?roleDefinitionName=='Azure AI User']" 2>$null | ConvertFrom-Json
|
|
if ($existing) {
|
|
Write-Host " role already assigned"
|
|
} else {
|
|
Write-Host " granting Azure AI User..."
|
|
$maxAttempts = 12
|
|
$granted = $false
|
|
for ($i = 1; $i -le $maxAttempts; $i++) {
|
|
$output = az role assignment create `
|
|
--assignee-object-id $principalId `
|
|
--assignee-principal-type ServicePrincipal `
|
|
--role 'Azure AI User' `
|
|
--scope $projectScope 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$granted = $true
|
|
break
|
|
}
|
|
if ($output -match 'Cannot find user or service principal in graph') {
|
|
Write-Host " attempt $i/$maxAttempts : MI not yet in AAD graph, retrying in 15s..."
|
|
Start-Sleep 15
|
|
continue
|
|
}
|
|
throw "az role assignment failed: $output"
|
|
}
|
|
if (-not $granted) {
|
|
throw "MI '$principalId' did not appear in AAD graph after $maxAttempts attempts."
|
|
}
|
|
Write-Host " granted (RBAC propagation may take 1-3 minutes)"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Done. Wait ~3 minutes after first-time grants before running the tests."
|