Skip to content
Closed
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
5 changes: 5 additions & 0 deletions sreagent-templates/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added the `cost-management` recipe for native Azure Cost Management, Resource Graph, Advisor, Pricing, Quota, and Microsoft Teams FinOps governance without FinOps Hubs or ADX.

### Fixed
- Updated `apply-extras.sh` and `Apply-Extras.ps1` to upload `PythonFunctionTool` and `KustoTool` as `ExtendedAgentTool`, upload tools before skills and subagents, propagate ConnectorV2 `spec.operations`, and deploy custom instructions to `/api/v2/agent/customInstructions`.
- Updated `assemble-agent.sh` and `Assemble-Agent.ps1` to bundle `custom-instructions.md`, and `verify-agent.sh` and `Verify-Agent.ps1` to verify custom instructions and tools.
- Discover available Azure SRE Agent regions from the selected subscription in Bash and PowerShell workflows, with all 20 live regions as the offline fallback.
- Made `export-agent.sh` detect and install PyYAML before export, with an isolated-environment fallback and actionable failure guidance.

Expand Down
1 change: 1 addition & 0 deletions sreagent-templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ cd sre-agent/sreagent-templates
| [azmon-lawappinsights](recipes/azmon-lawappinsights/) | Azure Monitor | Alert response with AppInsights + Log Analytics, skills, subagents, scheduled tasks |
| [pagerduty-law-vmcosmos](recipes/pagerduty-law-vmcosmos/) | PagerDuty | VM + CosmosDB + HTTP error investigation with knowledge files and skills |
| [dynatrace-mcp](recipes/dynatrace-mcp/) | Dynatrace | Dynatrace MCP connector for investigating application errors |
| [cost-management](recipes/cost-management/) | (none) | Azure cost management and FinOps governance with Microsoft Teams |
| [minimal](recipes/minimal/) | (none) | Minimal agent — just infra + RBAC, add your own connectors and skills |

Each recipe README has the full parameter list, example values, and post-deploy steps.
Expand Down
78 changes: 52 additions & 26 deletions sreagent-templates/bicep/Apply-Extras.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ function Get-DpToken {
return $tok
}

if ($extras.PSObject.Properties['customInstructions'] -and $extras.customInstructions -is [string]) {
if ($DpTokenAvailable) {
try {
$headers = @{ Authorization = "Bearer $(Get-DpToken)" }
$body = ConvertTo-Json -InputObject @{ instructions = $extras.customInstructions } -Compress
$url = "$AgentEndpoint/api/v2/agent/customInstructions"
$null = Invoke-RestMethod -TimeoutSec 30 -Uri $url -Method Put -Headers $headers -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ContentType 'application/json'
Write-Host ' ok custom instructions'
} catch {
Write-Host " FAILED - PUT custom instructions: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
Write-Host " ⚠ Custom instructions skipped (no data-plane token)" -ForegroundColor Yellow
$DpSkippedItems += 'customInstructions'
$DpSkippedItems = [System.Collections.Generic.List[string]]$DpSkippedItems
}
}

function Get-SafeETag {
param($Response)

Expand Down Expand Up @@ -937,7 +955,31 @@ if ($toolPermissions) {
}

# ═════════════════════════════════════════════════════════════════════════════
# 4g-1. skills — data-plane PUT
# 4g-1. tools — data-plane PUT
# Route: PUT /api/v2/extendedAgent/tools/{name}
# ═════════════════════════════════════════════════════════════════════════════
$toolItems = if ($extras.PSObject.Properties['tools']) { $extras.tools } else { $null }
$tlCount = ($toolItems | Measure-Object).Count
if ($tlCount -gt 0) {
if ($DpTokenAvailable) {
Write-Host "tools: $tlCount"
foreach ($tool in $toolItems) {
$name = if ($tool.metadata) { $tool.metadata.name } else { $tool.name }
$props = if ($tool.spec) { $tool.spec } else { $tool.properties }
$toolType = if ($null -ne $tool.kind) { $tool.kind } else { 'Tool' }
DataPlane-PutExtended -Kind "tools" -Name $name -Type $toolType -Tags @() -Properties $props
}
} else {
Write-Host "tools: $tlCount - WARNING skipped (no data-plane token)"
foreach ($tl in $toolItems) {
$tlName = if ($tl.metadata) { $tl.metadata.name } else { $tl.name }
$DpSkippedItems.Add("tool/$tlName")
}
}
}

# ═════════════════════════════════════════════════════════════════════════════
# 4g-2. skills — data-plane PUT
# Route: PUT /api/v2/extendedAgent/skills/{name}
# ═════════════════════════════════════════════════════════════════════════════
$skillItems = if ($extras.PSObject.Properties['skills']) { $extras.skills } else { $null }
Expand Down Expand Up @@ -966,7 +1008,7 @@ if ($skCount -gt 0) {
}

# ═════════════════════════════════════════════════════════════════════════════
# 4g-2. subagents — data-plane PUT
# 4g-3. subagents — data-plane PUT
# Route: PUT /api/v2/extendedAgent/agents/{name}
# ═════════════════════════════════════════════════════════════════════════════
$subagentItems = if ($extras.PSObject.Properties['subagents']) { $extras.subagents } else { $null }
Expand All @@ -988,29 +1030,6 @@ if ($saCount -gt 0) {
}
}

# ═════════════════════════════════════════════════════════════════════════════
# 4g-3. tools — data-plane PUT
# Route: PUT /api/v2/extendedAgent/tools/{name}
# ═════════════════════════════════════════════════════════════════════════════
$toolItems = if ($extras.PSObject.Properties['tools']) { $extras.tools } else { $null }
$tlCount = ($toolItems | Measure-Object).Count
if ($tlCount -gt 0) {
if ($DpTokenAvailable) {
Write-Host "tools: $tlCount"
foreach ($tl in $toolItems) {
$name = if ($tl.metadata) { $tl.metadata.name } else { $tl.name }
$props = if ($tl.spec) { $tl.spec } else { $tl.properties }
DataPlane-PutExtended -Kind "tools" -Name $name -Type "Tool" -Tags @() -Properties $props
}
} else {
Write-Host "tools: $tlCount - WARNING skipped (no data-plane token)"
foreach ($tl in $toolItems) {
$tlName = if ($tl.metadata) { $tl.metadata.name } else { $tl.name }
$DpSkippedItems.Add("tool/$tlName")
}
}
}

# ═════════════════════════════════════════════════════════════════════════════
# 4f-4. ConnectorV2 — managed connection, runtime access, and MCP tool binding
# ═════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -1058,10 +1077,17 @@ if ($cv2Count -gt 0) {
continue
}

$connectorBinding = [ordered]@{
name = $apiName
connectionName = $connectionName
}
if ($null -ne $spec.operations) {
$connectorBinding.operations = @($spec.operations)
}
$mcpBody = [ordered]@{
properties = [ordered]@{
description = $displayName
connectors = @(@{ name = $apiName; connectionName = $connectionName })
connectors = @($connectorBinding)
}
}
if ($spec.requireApprovalTools) {
Expand Down
5 changes: 5 additions & 0 deletions sreagent-templates/bicep/Assemble-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,11 @@ $extrasObj = [ordered]@{
connectors = $extrasConnectors
}

$customInstructionsFile = Join-Path $ConfigDir 'custom-instructions.md'
if (Test-Path $customInstructionsFile -PathType Leaf) {
$extrasObj['customInstructions'] = [System.IO.File]::ReadAllText((Resolve-Path $customInstructionsFile).Path)
}

$extrasObj | ConvertTo-Json -Depth 32 | Out-File -FilePath $ExtrasFile -Encoding utf8NoBOM

# Merge admin settings if present (adminUsers for cross-tenant access)
Expand Down
72 changes: 47 additions & 25 deletions sreagent-templates/bicep/apply-extras.sh
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,25 @@ dataplane_post_json() {
# Reusable: get a data-plane bearer (defined here so multipart helper can use it)
_dp_token() { az account get-access-token --resource https://azuresre.dev --query accessToken -o tsv 2>/dev/null; }

CUSTOM_BODY=$(jq -c '.customInstructions | select(type == "string") | {instructions: .}' "$FILE")
if [[ -n "$CUSTOM_BODY" ]]; then
if [[ "$DP_TOKEN_AVAILABLE" == true ]]; then
TOKEN=$(_dp_token)
result=$(curl -sS -w "\n%{http_code}" -X PUT "${AGENT_ENDPOINT}/api/v2/agent/customInstructions" \
-H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \
--data-binary "$CUSTOM_BODY" 2>&1)
http_code=$(echo "$result" | tail -1)
if [[ "$http_code" =~ ^(200|201|202|204)$ ]]; then
echo " ok custom instructions"
else
echo " FAILED — PUT custom instructions (HTTP ${http_code})"
fi
else
echo " ⚠ Custom instructions skipped (no data-plane token)"
DP_SKIPPED_ITEMS+=("customInstructions")
fi
fi

# ---------------------------------------------------------------------------
# Helper: PUT to v2 extendedAgent dataplane (skills/subagents/hooks/commonprompts/etc.).
# Body shape (from Agent.Web/ApiResources/ApiRequestEnvelope.cs):
Expand Down Expand Up @@ -815,6 +834,7 @@ if [[ "$count" -gt 0 ]]; then
cv2_pvs=$(echo "$cv2_spec" | jq -c '.parameterValueSet // null')
cv2_pv=$(echo "$cv2_spec" | jq -c '.parameterValues // null')
cv2_rat=$(echo "$cv2_spec" | jq -c '.requireApprovalTools // null')
cv2_ops=$(echo "$cv2_spec" | jq -c '.operations // null')

TOKEN=$(_dp_token)

Expand Down Expand Up @@ -851,8 +871,8 @@ if [[ "$count" -gt 0 ]]; then

# Step 3: Create MCP server config (links connection to MCP tools)
mcp_body=$(jq -nc --arg desc "$cv2_display" --arg cn "$cv2_conn" --arg api "$cv2_api" \
--argjson rat "$cv2_rat" \
'{properties: {description: $desc, connectors: [{name: $api, connectionName: $cn}]}} + (if $rat != null then {runtimeMcpConfiguration: {requireApprovalTools: $rat}} else {} end)')
--argjson rat "$cv2_rat" --argjson ops "$cv2_ops" \
'{properties: {description: $desc, connectors: [{name: $api, connectionName: $cn} + (if $ops != null then {operations: $ops} else {} end)]}} + (if $rat != null then {runtimeMcpConfiguration: {requireApprovalTools: $rat}} else {} end)')
TOKEN=$(_dp_token)
mcp_result=$(curl -sS -w "\n%{http_code}" -X PUT \
"${AGENT_ENDPOINT}/api/v2/connectorV2/mcpservers/${cv2_conn}" \
Expand Down Expand Up @@ -948,7 +968,30 @@ if [[ "$count" -gt 0 ]]; then
done
fi

# 4i-1. skills — data-plane PUT
# 4i-1. tools — data-plane PUT
# Route: PUT /api/v2/extendedAgent/tools/{name}
# Body: { name, type: <declared kind>, tags: [], properties: { ... tool spec } }
count=$(jq '.tools // [] | length' "$FILE")
if [[ "$count" -gt 0 ]]; then
if [[ "$DP_TOKEN_AVAILABLE" == "true" ]]; then
echo "tools: ${count}"
EXTRAS_FILE="$FILE"
for i in $(seq 0 $((count - 1))); do
name=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE")
props=$(jq -c --argjson i "$i" '.tools[$i].spec' "$FILE")
object_type=$(jq -r ".tools[$i].kind // \"Tool\"" "$EXTRAS_FILE")
dataplane_put_extended "tools" "$name" "$object_type" "[]" "$props"
done
else
echo "tools: ${count} — ⚠ skipped (no data-plane token)"
for i in $(seq 0 $((count - 1))); do
tname=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE")
DP_SKIPPED_ITEMS+=("tool/${tname}")
done
fi
fi

# 4i-2. skills — data-plane PUT
# Route: PUT /api/v2/extendedAgent/skills/{name}
# Body: { name, type: "Skill", tags: [], properties: { name, description, tools, skillContent, additionalFiles } }
count=$(jq '.skills // [] | length' "$FILE")
Expand All @@ -975,7 +1018,7 @@ if [[ "$count" -gt 0 ]]; then
fi
fi

# 4i-2. subagents — data-plane PUT
# 4i-3. subagents — data-plane PUT
# Route: PUT /api/v2/extendedAgent/agents/{name}
# Body: { name, type: "ExtendedAgent", tags: [], properties: { instructions, handoffDescription, tools, ... } }
count=$(jq '.subagents // [] | length' "$FILE")
Expand All @@ -996,27 +1039,6 @@ if [[ "$count" -gt 0 ]]; then
fi
fi

# 4i-3. tools — data-plane PUT
# Route: PUT /api/v2/extendedAgent/tools/{name}
# Body: { name, type: "Tool", tags: [], properties: { ... tool spec } }
count=$(jq '.tools // [] | length' "$FILE")
if [[ "$count" -gt 0 ]]; then
if [[ "$DP_TOKEN_AVAILABLE" == "true" ]]; then
echo "tools: ${count}"
for i in $(seq 0 $((count - 1))); do
name=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE")
props=$(jq -c --argjson i "$i" '.tools[$i].spec' "$FILE")
dataplane_put_extended "tools" "$name" "Tool" "[]" "$props"
done
else
echo "tools: ${count} — ⚠ skipped (no data-plane token)"
for i in $(seq 0 $((count - 1))); do
tname=$(jq -r --argjson i "$i" '.tools[$i].metadata.name' "$FILE")
DP_SKIPPED_ITEMS+=("tool/${tname}")
done
fi
fi

# 4i. Webhook bridge Logic App — auto-deploy if httpTriggers exist and enableWebhookBridge is set
# Solves the chicken-and-egg: trigger URL is only known after httpTrigger creation above.
if [[ -n "$HTTP_TRIGGER_URL" ]]; then
Expand Down
6 changes: 6 additions & 0 deletions sreagent-templates/bicep/assemble-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,12 @@ jq -n \
"connectorV2": $connectorV2
}' > "$EXTRAS_FILE"

if [[ -f "${DIR}/custom-instructions.md" ]]; then
EXTRAS_WITH_INSTRUCTIONS=$(jq --rawfile instructions "${DIR}/custom-instructions.md" \
'.customInstructions = $instructions' "$EXTRAS_FILE")
printf '%s\n' "$EXTRAS_WITH_INSTRUCTIONS" > "$EXTRAS_FILE"
fi

# Merge admin settings if present (adminUsers for cross-tenant access)
if [[ -f "${DIR}/admin-settings.json" ]]; then
_log "Merging admin-settings.json (adminUsers) into extras"
Expand Down
40 changes: 40 additions & 0 deletions sreagent-templates/bin/ps/Verify-Agent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,25 @@ Add-Check 'Upgrade channel' ($Props | Invoke-Jq -Raw -Filter '.upgradeChannel
Add-Check 'Model provider' ($Props | Invoke-Jq -Raw -Filter '.modelProvider') (Get-Exp '.agent.defaultModelProvider')
Add-Check 'Incident platform' ($Props | Invoke-Jq -Raw -Filter '.incidentPlatform') (Get-Exp '.agent.incidentPlatform')

if ($ExpectedConfig) {
$customExpected = $ExpectedConfig | ConvertFrom-Json
if ($customExpected.PSObject.Properties['customInstructions']) {
$customStatus = 'mismatch'
try {
$customResponse = Invoke-RestMethod -Uri "$Endpoint/api/v2/agent/customInstructions" `
-Headers @{ Authorization = "Bearer $Token" } -TimeoutSec 30
if ($customExpected.customInstructions -is [string] -and $customResponse.instructions -is [string] -and
(($customExpected.customInstructions -replace "\r\n?", "`n").Trim() -ceq
($customResponse.instructions -replace "\r\n?", "`n").Trim())) {
$customStatus = 'match'
}
} catch {
$customStatus = 'unavailable'
}
Add-Check 'Custom instructions' $customStatus 'match'
}
}

# ─────────────────────────── Connectors (ARM) ───────────────────────────

$Connectors = Invoke-Arm '/DataConnectors'
Expand Down Expand Up @@ -331,6 +350,27 @@ if ($ExpManagedConnNames) {
Add-InfoRow 'Managed connector names' $ManagedConnNames
}

# ─────────────────────────── Custom tools ───────────────────────────

$HasExpectedTools = $false
$ExpToolCount = '-'
if ($ExpectedConfig) {
$expectedTools = ($ExpectedConfig | ConvertFrom-Json).PSObject.Properties['tools']
$HasExpectedTools = $null -ne $expectedTools -and $null -ne $expectedTools.Value
}
if (-not $HasExpectedTools -and $Expected) {
$toolsDir = Join-Path $Expected 'config/tools'
if (Test-Path $toolsDir -PathType Container) {
$HasExpectedTools = $true
$ExpToolCount = [string]@(Get-ChildItem -Path $toolsDir -Filter '*.yaml' -File).Count
}
}
if ($HasExpectedTools) {
$Tools = Invoke-Dp '/api/v2/extendedAgent/tools' -Collection
$ToolCt = $Tools | Invoke-Jq -Filter '.value | length'
Add-Check 'Custom tools' $ToolCt (Get-Exp '.tools' $ExpToolCount) $CollectionCountMode
}

# ─────────────────────────── Skills ───────────────────────────

$Skills = Invoke-Dp '/api/v1/extendedAgent/skills' -Collection
Expand Down
Loading
Loading