diff --git a/docs-mslearn/.openpublishing.redirection.finops.json b/docs-mslearn/.openpublishing.redirection.finops.json
index ab20d5dbd..b2caf8eba 100644
--- a/docs-mslearn/.openpublishing.redirection.finops.json
+++ b/docs-mslearn/.openpublishing.redirection.finops.json
@@ -130,6 +130,11 @@
"redirect_url": "/cloud-computing/finops/toolkit/workbooks/customize-workbooks",
"redirect_document_id": false
},
+ {
+ "source_path_from_root": "/finops/finops/toolkit/hubs/configure-sre.md",
+ "redirect_url": "/cloud-computing/finops/toolkit/sre-agent/overview",
+ "redirect_document_id": false
+ },
{
"source_path_from_root": "/finops/finops/framework/manage/culture.md",
"redirect_url": "/cloud-computing/finops/framework/manage/education",
diff --git a/docs-mslearn/TOC.yml b/docs-mslearn/TOC.yml
index 4d48e0aff..4edc706b0 100644
--- a/docs-mslearn/TOC.yml
+++ b/docs-mslearn/TOC.yml
@@ -54,7 +54,7 @@
href: framework/optimize/optimize-cloud-usage-cost.md
- name: Architecting for cloud
href: framework/optimize/architecting.md
- - name: Workload optimization
+ - name: Usage optimization
href: framework/optimize/workloads.md
- name: Rate optimization
href: framework/optimize/rates.md
@@ -154,6 +154,26 @@
href: toolkit/hubs/upgrade.md
- name: Compatibility guide
href: toolkit/hubs/compatibility.md
+ - name: FinOps toolkit SRE Agent
+ items:
+ - name: Overview
+ href: toolkit/sre-agent/overview.md
+ - name: Deploy
+ href: toolkit/sre-agent/deploy.md
+ - name: Agents and skills
+ href: toolkit/sre-agent/agents.md
+ - name: Tools
+ href: toolkit/sre-agent/tools.md
+ - name: Scheduled tasks
+ href: toolkit/sre-agent/scheduled-tasks.md
+ - name: Knowledge and memory
+ href: toolkit/sre-agent/knowledge.md
+ - name: Security and permissions
+ href: toolkit/sre-agent/security.md
+ - name: Troubleshooting
+ href: toolkit/sre-agent/troubleshooting.md
+ - name: Template reference
+ href: toolkit/sre-agent/template.md
- name: Power BI
items:
- name: Overview
@@ -170,7 +190,7 @@
href: toolkit/power-bi/invoicing.md
- name: Rate optimization report
href: toolkit/power-bi/rate-optimization.md
- - name: Workload optimization report
+ - name: Usage optimization report
href: toolkit/power-bi/workload-optimization.md
- name: Data ingestion report
href: toolkit/power-bi/data-ingestion.md
diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md
index 04f1298d2..762824bfb 100644
--- a/docs-mslearn/toolkit/changelog.md
+++ b/docs-mslearn/toolkit/changelog.md
@@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
-ms.date: 05/19/2026
+ms.date: 06/04/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
@@ -25,55 +25,49 @@ This article summarizes the features and enhancements in each release of the Fin
The following section lists features and enhancements that are currently in development.
--->
-
-
-
-## v15
-
-_Released June 2026_
-
-### [Implementing FinOps guide](../implementing-finops-guide.md) v15
-
-- **Fixed**
- - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` in the [data dictionary](help/data-dictionary.md) to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)).
-
-### Claude Code plugin v15
+### Claude Code plugin v15.0.0
- **Added**
- Added Claude Code plugin with skills for FinOps hubs and Azure Cost Management ([#2043](https://github.com/microsoft/finops-toolkit/pull/2043)).
- Added 4 agents (CFO, FinOps practitioner, database query, hubs agent), 5 commands (`/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, `/ftk-ytd-report`, `/ftk-cost-optimization`), and an output style.
- Linked to the existing KQL query catalog in `src/queries/` from the plugin.
-### [FinOps hubs](hubs/finops-hubs-overview.md) v15
+### Bicep Registry module pending updates
-- **Changed**
- - Added a callout to the `config_RunBackfillJob` backfill option clarifying that it isn't supported on Microsoft Customer Agreement (MCA) billing accounts or billing profiles ([#2113](https://github.com/microsoft/finops-toolkit/issues/2113)).
+- Cost Management export modules for subscriptions and resource groups.
### [Data dictionary](help/data-dictionary.md) updates
- **Fixed**
- Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)).
-
-### [Optimization engine](optimization-engine/overview.md) updates
-
-- **Fixed**
- - Removed call to Azure Classic administrators endpoint (deprecated on May 1, 2026) from Azure RBAC assignments exports ([#2142](https://github.com/microsoft/finops-toolkit/issues/2142)).
-
-->
-
-> [!div class="nextstepaction"]
-> [Download](https://github.com/microsoft/finops-toolkit/releases/tag/v15)
-> [!div class="nextstepaction"]
-> [Full changelog](https://github.com/microsoft/finops-toolkit/compare/v14...v15)
-
-
+
## v14
_Released April 2026_
+### Claude Code plugin v13.0.0
+
+- **Added**
+ - Added Claude Code plugin with skills for FinOps hubs and Azure Cost Management.
+ - Added 4 agents (CFO, FinOps practitioner, database query, hubs agent), 5 commands (`/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, `/ftk-ytd-report`, `/ftk-cost-optimization`), and an output style.
+ - Linked to the existing KQL query catalog in `src/queries/` from the plugin.
+
+### [SRE agent](sre-agent/overview.md)
+
+- **Added**
+ - Added Azure SRE Agent template with a packaged Deploy to Azure path, `deploy.sh` local CLI automation, and Bicep infrastructure.
+ - Added 5 subagents (`finops-practitioner`, `azure-capacity-manager`, `chief-financial-officer`, `ftk-database-query`, `ftk-hubs-agent`), 3 skills, 50 tools, and 1 Kusto MCP connector.
+ - Added 19 scheduled tasks (daily, weekly, monthly, semiannual, and quarterly cadences) with Teams channel delivery.
+ - Added 6 knowledge documents for agent onboarding, artifact verification, Teams notification patterns, known issues, document index guidance, and FinOps Toolkit output style.
+ - Added FinOps toolkit SRE Agent documentation pages for Microsoft Learn.
+- **Changed**
+ - Set agent action mode to Autonomous so scheduled tasks can deliver reports without human approval.
+ - Switched scheduled task persistence from legacy CLI creation to manifest apply for idempotent re-runs.
+ - Replaced "save to knowledge base" instructions with `#remember` for operational notes across all scheduled tasks.
+
### [Implementing FinOps guide](../implementing-finops-guide.md) v14
- **Added**
diff --git a/docs-mslearn/toolkit/hubs/configure-sre.md b/docs-mslearn/toolkit/hubs/configure-sre.md
new file mode 100644
index 000000000..8a1810137
--- /dev/null
+++ b/docs-mslearn/toolkit/hubs/configure-sre.md
@@ -0,0 +1,249 @@
+---
+title: Configure an SRE agent for FinOps hubs
+description: Learn how to configure an Azure SRE agent to connect to your FinOps hub for scheduled cost analysis, capacity monitoring, and reporting.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: how-to
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: micflan
+# customer intent: As a FinOps hub admin, I want to connect an Azure SRE agent to my hub so that I can receive scheduled cost reports, anomaly detection, and capacity monitoring in Teams.
+---
+
+# Configure an SRE agent for FinOps hubs
+
+[Azure SRE Agent](/azure/sre-agent/overview) supports agent-based operational workflows. This article shows how to connect Azure SRE Agent to a [FinOps hub](finops-hubs-overview.md), configure scheduled cost analysis and capacity checks from the [SRE agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent), and send results to Teams with the [Teams notification connector](/azure/sre-agent/send-notifications).
+
+
+
+## Prerequisites
+
+- [Deployed a FinOps hub instance](finops-hubs-overview.md#create-a-new-hub) with Data Explorer.
+- [Configured scopes](configure-scopes.md) and ingested data successfully.
+- Permissions to create deployed resources, such as **Contributor** on the subscription when the template creates the agent resource group. You also need permissions to assign roles at subscription, target resource group, and agent scopes, such as **Role Based Access Control Administrator**, **User Access Administrator**, or **Owner** on those scopes. [Learn more](/azure/role-based-access-control/built-in-roles).
+- The `Microsoft.App` resource provider [registered](/azure/azure-resource-manager/management/resource-providers-and-types#register-resource-provider) on the subscription.
+- For local CLI deployments only: [Azure CLI](/cli/azure/install-azure-cli) 2.60 or later, `curl`, `jq`, `python3`, and `bash` available locally for the [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent).
+
+
+
+## Review deployed resources
+
+The [SRE agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) deploys a single Azure SRE agent with these resources and configuration objects:
+
+| Component | Count | Description |
+|-----------|-------|-------------|
+| SRE agent | 1 | Stable [`Microsoft.App/agents`](/azure/sre-agent/overview) resource in [autonomous mode](/azure/sre-agent/run-modes) |
+| Managed identities | 1-2 | System-assigned managed identity for the agent; portal deployments also create a user-assigned identity for the deployment script |
+| Log Analytics | 1 | Workspace for agent telemetry |
+| Application Insights | 1 | Linked to Log Analytics for monitoring |
+| Target resource group RBAC | 3-4 per target group | Reader, Monitoring Reader, Log Analytics Reader, and Contributor when `accessLevel` is `High` |
+| Azure Data Explorer role (optional) | 1 | `AllDatabasesViewer` when Azure Data Explorer parameters are provided |
+| Subagents | 5 | `finops-practitioner`, `azure-capacity-manager`, `chief-financial-officer`, `ftk-database-query`, `ftk-hubs-agent` |
+| Skills | 3 | `azure-capacity-management`, `azure-cost-management`, `finops-toolkit` |
+| Tools | 50 | Kusto and Python tools for cost, capacity, governance, and reporting workflows |
+| Connector | 1 | Kusto MCP connector to the FinOps hub Azure Data Explorer cluster |
+| Scheduled tasks | 19 | Reports at daily, weekly, monthly, semiannual, and quarterly cadences |
+| Knowledge docs | 6 | Onboarding, artifact verification, Teams notification patterns, known issues, document index guidance, and the FinOps Toolkit output style |
+
+
+
+## Deploy the SRE agent
+
+Use the Azure portal deployment for the same one-click experience as other FinOps toolkit templates. The template creates the Azure resources and runs an embedded deployment script to apply the FinOps hub recipe assets.
+
+
+> [!div class="nextstepaction"]
+> [Deploy to Azure](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2FcreateUiDefinition.json)
+
+
+You can also use the [local deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent). It is copied from the Microsoft SRE Agent starter lab and updated for the FinOps toolkit. It uses Azure CLI + Bicep directly and doesn't use `azd`:
+
+```bash
+cd src/templates/sre-agent
+
+bash bin/deploy.sh \
+ --recipe recipes/finops-hub \
+ --subscription \
+ --resource-group \
+ --name \
+ --location \
+ --cluster-uri https://..kusto.windows.net/Hub \
+ [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/]
+```
+
+The portal deployment is the customer-facing template entry point. `bin/deploy.sh` remains available for local CLI automation.
+
+The local [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent):
+
+1. Sets the `az` CLI context to the target subscription. This step is required for [B2B tenant environments](#troubleshoot-b2b-tenant-environments).
+2. Runs a subscription-scoped Azure CLI + Bicep deployment from `infra/main.bicep`.
+3. Runs `bin/apply-extras.sh`, which applies the recipe assets that are not deployed by Bicep.
+
+The extras step follows the Microsoft SRE Agent template pattern: connectors and KnowledgeFile sources are applied as SRE Agent child resources, while built-in tool configuration, 3 skills, 5 subagents, 50 tools, and 19 scheduled tasks are applied through the SRE Agent data plane.
+
+### Grant the optional Azure Data Explorer viewer role
+
+To grant the agent's managed identity the `AllDatabasesViewer` role on your Azure Data Explorer cluster, pass `--cluster-uri`. The deployment resolves the cluster ARM resource ID from the URI when the cluster is in the target subscription. If the cluster is in another subscription or can't be resolved, pass `--cluster-resource-id` explicitly. The deployment uses the [Azure Data Explorer role module](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep):
+
+```bash
+--cluster-uri https://..kusto.windows.net/Hub \
+--cluster-resource-id /subscriptions/.../resourceGroups//providers/Microsoft.Kusto/clusters/
+```
+
+If your Azure Data Explorer cluster has `publicNetworkAccess` set to `Disabled`, the deployment still creates all SRE Agent resources, assigns `AllDatabasesViewer`, and creates the `finops-hub-kusto` connector. The script also prints a warning because hosted Azure SRE Agent can't run direct KQL queries against private endpoint ADX clusters. Review the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations), then decide whether to enable public query access for the cluster. Until public query access is enabled or Azure SRE Agent adds private endpoint ADX query support, the connector is expected to remain unhealthy.
+
+### Redeploy an existing agent
+
+To update an existing agent, rerun the [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) with the same resource group and agent name:
+
+```bash
+bash bin/deploy.sh \
+ --recipe recipes/finops-hub \
+ --subscription \
+ --resource-group \
+ --name \
+ --location \
+ --cluster-uri https://..kusto.windows.net/Hub \
+ [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/]
+```
+
+### Delete the deployment
+
+To delete Azure resources, delete the resource group that contains the SRE Agent resources after confirming no other resources share it:
+
+```bash
+az group delete --subscription --name
+```
+
+
+
+## Verify the deployment
+
+After `bin/deploy.sh` completes, use the template's [post-deployment verification guidance](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent#verify):
+
+1. Confirm `bin/apply-extras.sh` completed without errors.
+2. Open [sre.azure.com](https://sre.azure.com), switch to the directory that contains your subscription, and select your agent.
+3. Confirm 5 subagents, 3 skills, and 50 tools appear in **Builder**.
+4. Go to **Scheduled tasks** and confirm 19 tasks are listed and active.
+5. Ask the agent: `What knowledge documents do you have?` and confirm the shipped knowledge is available.
+
+
+
+## Configure Teams notifications
+
+Scheduled tasks can send reports to a Teams channel through the [Teams notification connector](/azure/sre-agent/send-notifications). The connector requires interactive OAuth setup in the portal.
+
+1. Open [sre.azure.com](https://sre.azure.com), open your agent, then go to **Builder** > **Connectors**.
+2. Select **Add connector** > **Send notification (Microsoft Teams)**.
+3. Sign in with your Microsoft 365 account and paste the channel URL from **Get link to channel** in Teams.
+4. Select the agent's managed identity and save.
+5. Test from chat: `Post a test message to our Teams channel saying "FinOps SRE agent connected."`
+
+Use the built-in `PostTeamsMessage` tool from the [Teams notification guidance](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md). Don't call the Microsoft Graph API or the connection's `dynamicInvoke` endpoint directly because that path returns a 403 error for this connector configuration.
+
+For Outlook notifications, follow the same pattern with the **Outlook Tools (Office 365 Outlook)** connector. See [Send notifications](/azure/sre-agent/send-notifications) for details.
+
+
+
+## Review scheduled tasks
+
+The template deploys 19 scheduled tasks from the [`recipes/finops-hub/automations/scheduled-tasks`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks) folder. When the Teams connector is configured, each task posts its final report to the connected channel:
+
+| Task | Agent | Schedule | What it reports |
+|------|-------|----------|-----------------|
+| HubsHealthCheck | finops-practitioner | Daily 6:00 AM | Hub version, data freshness, and pipeline status |
+| CapacityDailyMonitor | finops-practitioner | Daily 6:30 AM | Quota usage, CRG utilization, and alert status |
+| ComputeUtilizationTrend | finops-practitioner | Weekly Monday 7:00 AM | VM quota utilization trends across subscriptions and regions |
+| CostOptimization | finops-practitioner | Weekly Monday 8:00 AM | Orphaned resources, rightsizing, and commitment opportunities |
+| CapacityWeeklySupplyReview | finops-practitioner | Weekly Monday 8:00 AM | Quota headroom, CRG cost-waste audit, and benefit recommendations |
+| NonComputeQuotaAudit | finops-practitioner | Weekly Tuesday 7:00 AM | Storage, network, and non-compute quota risks |
+| DbQuotaAudit | finops-practitioner | Weekly Wednesday 7:00 AM | Database quota and region or zone access risks |
+| SkuAvailabilityAudit | finops-practitioner | Weekly Wednesday 7:30 AM | Regional SKU availability and deployment blockers |
+| MonitoringScopeValidation | finops-practitioner | Weekly Thursday 9:00 AM | Subscription monitoring coverage and hub freshness |
+| BenefitRecommendationReview | finops-practitioner | Weekly Friday 8:00 AM | Reservation and savings plan recommendations with finance framing |
+| StoragePaasGrowthForecast | finops-practitioner | Monthly 1st 8:00 AM | Storage and PaaS quota growth forecast |
+| AdvisorSuppressionReview | finops-practitioner | Monthly 1st 9:00 AM | Active Advisor suppression age, expiration, and risk |
+| CapacityMonthlyPlanning | finops-practitioner | Monthly 1st 9:00 AM | Demand forecast, capacity request pipeline, and governance review |
+| AIWorkloadCostAnalysis | finops-practitioner | Monthly 1st 10:00 AM | AI token economics, model efficiency, and cost allocation |
+| Monthly | finops-practitioner | Monthly on the 5th at 5:15 PM | Month-over-month cost analysis using Kusto evidence delegated to ftk-database-query |
+| BudgetCoverageAudit | finops-practitioner | Monthly 15th 8:00 AM | Subscription budget coverage and missing guardrails |
+| AlertCoverageAudit | finops-practitioner | Monthly 16th 8:00 AM | Cost anomaly alert coverage across active subscriptions |
+| Semiannual | finops-practitioner | January 5 and July 5 at 9:00 AM | Semiannual year-over-year finance analysis with forecast and CFO consultation |
+| CapacityQuarterlyStrategy | finops-practitioner | Quarterly 9:00 AM | FinOps capability maturity, commitment alignment, and architecture review |
+
+Each scheduled task reads the uploaded knowledge documents before it starts and applies `ftk-output-style.md` for evidence, formatting, FinOps capability mapping, confidence, and caveat conventions. Send financial results to Teams through the configured [notification connector](/azure/sre-agent/send-notifications). Save only operational notes, such as tool errors, workarounds, and patterns, to agent [memory](/azure/sre-agent/memory) with `#remember`; don't save financial data.
+
+
+
+## Troubleshoot B2B tenant environments
+
+In B2B environments, the Azure subscription and Azure SRE Agent resource can live in a different Microsoft Entra tenant than your Microsoft 365 home tenant. The deployment script sets the active subscription before deployment to align the CLI context with the resource tenant.
+
+If [sre.azure.com](https://sre.azure.com) shows the agent correctly but `bin/apply-extras.sh` cannot get an SRE Agent data-plane token, use these B2B tenant checks:
+
+1. Confirm the active Azure CLI context points at the subscription that owns the SRE agent resource.
+2. Re-authenticate against the tenant that owns the subscription.
+3. Run `az login --scope https://azuresre.dev/.default`, then rerun `bin/deploy.sh` or `bin/apply-extras.sh`.
+
+Browser success with CLI failure indicates that the CLI token was issued for the wrong tenant. The [deployment script](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) runs `az account set --subscription` before deployment to set the target subscription context.
+
+
+
+## Review built-in capabilities
+
+Azure SRE Agent includes platform capabilities that are on by default in this template:
+
+- **Workspace tools**: Azure SRE Agent can run file, terminal, and Python operations in a sandboxed environment for data analysis, chart generation, and report formatting. The [Bicep template](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/infra/modules/sre-agent.bicep) deploys the stable agent API and sets `experimentalSettings.EnableSandboxGroup` and `experimentalSettings.EnableWorkspaceTools`. See [Deep context](/azure/sre-agent/workspace-tools).
+- **DocsGuide**: DocsGuide provides Azure documentation grounding for agent responses. See [Use DocsGuide](/azure/sre-agent/use-docsguide).
+- **Visualization**: Built-in chart and table rendering for investigation results. See [Tools](/azure/sre-agent/tools).
+- **Memory**: Memory stores operational knowledge across sessions. See [Memory and knowledge](/azure/sre-agent/memory).
+
+The analytical subagents include `execute_python` where they need calculations, charts, tables, and downloadable artifacts. `finops-practitioner` orchestrates scheduled analysis, `ftk-database-query` owns Kusto evidence, `azure-capacity-manager` owns Azure capacity evidence, and `chief-financial-officer` provides finance and leadership consultation.
+
+
+
+## Review supported regions
+
+The Azure SRE Agent deployment supports `australiaeast`, `canadacentral`, `eastus2`, `francecentral`, `koreacentral`, `swedencentral`, and `uksouth`. The [Bicep template](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/main.bicep) restricts the `location` parameter with `@allowed`.
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20FinOps%20hubs%3F/cvaQuestion/How%20valuable%20are%20FinOps%20hubs%3F/surveyId/FTK/bladeName/Hubs/featureName/ConfigureSRE)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20FinOps%20hubs%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+
+Related solutions:
+
+- [Configure AI agents for FinOps hubs](configure-ai.md)
+- [FinOps hubs](finops-hubs-overview.md)
+- [FinOps toolkit Power BI reports](../power-bi/reports.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/agents.md b/docs-mslearn/toolkit/sre-agent/agents.md
new file mode 100644
index 000000000..836779c74
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/agents.md
@@ -0,0 +1,363 @@
+---
+title: Specialist agents and skills (Azure SRE Agent in the FinOps toolkit)
+description: Understand how the FinOps toolkit configures Azure SRE Agent with specialist agents, tools, skills, and knowledge to automate FinOps and capacity management work.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: concept-article
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to understand how the FinOps toolkit's specialist agents and skills work so that I can route work to the right specialist.
+---
+
+# Specialist agents and skills (Azure SRE Agent in the FinOps toolkit)
+
+The FinOps toolkit configures [Azure SRE Agent](/azure/sre-agent/overview) with a multi-agent architecture aligned to the canonical FinOps Framework. One orchestrator receives prompts and scheduled tasks, then delegates work to specialist subagents with focused FinOps, finance, capacity, database, and hub operations expertise.
+
+The template configures 5 subagents, 3 skills, 50 tools (37 generated Kusto tools and 13 Python tools), and a FinOps hub connector. The orchestrator keeps the experience simple. The specialist agents keep answers grounded in the right domain.
+
+
+
+## finops-practitioner
+
+Domain: FinOps practice guidance, cost allocation, optimization, anomaly response, AI cost management, governance, and operating-model design.
+
+The `finops-practitioner` agent helps teams apply FinOps principles to real Azure cost and usage questions. It assesses business context, maturity, stakeholders, and trade-offs before recommending actions. It leads scheduled FinOps workflows and delegates evidence collection to specialists.
+
+What it does:
+
+- Guides cost allocation, shared-cost strategy, showback, and chargeback.
+- Orchestrates cost anomaly and cost-driver investigations using evidence from `ftk-database-query`.
+- Plans Usage Optimization, Rate Optimization, Governance, Policy & Risk, and practice health improvements.
+- Translates FinOps concepts into practical Microsoft Cost Management and FinOps toolkit steps.
+- Keeps recommendations maturity-aware across Crawl, Walk, and Run stages.
+
+Key tool routing:
+
+- The orchestrator has no direct operational tools.
+- Delegation to `ftk-database-query` for every Kusto, FOCUS, `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` evidence request.
+- Delegation to `azure-capacity-manager` for quota, capacity reservation, SKU, region, zone, and AI/GPU capacity evidence.
+- Delegation to `ftk-hubs-agent` for Resource Graph, data freshness, hub infrastructure, budget deployment, anomaly alert deployment, and Advisor suppression tools.
+
+When it's invoked:
+
+- A user asks for FinOps practice guidance, anomaly triage, allocation, governance, reporting, optimization, or maturity planning.
+- A scheduled task needs recurring cost visibility, optimization review, AI cost analysis, or FinOps practice health checks.
+- The orchestrator needs a general FinOps owner before handing deeper Kusto or finance work to another specialist.
+
+Example prompts:
+
+- "Help me design a showback model for shared platform costs across engineering teams."
+- "Investigate why our AI costs increased this month and recommend the first three actions."
+- "Create a Crawl-Walk-Run plan for improving tagging, budgets, anomaly response, and optimization."
+- "Review our reservation and savings plan opportunities and explain which FinOps capabilities we should mature first."
+
+Expected output:
+
+- A practical FinOps recommendation with business context, maturity assumptions, trade-offs, and next steps.
+- Evidence-backed cost drivers, optimization opportunities, governance actions, and stakeholder guidance.
+- Clear separation between what the agent knows from tool evidence and what needs more data.
+
+Use this agent versus another:
+
+- Use `finops-practitioner` first when the question mixes cost analysis, FinOps process, governance, allocation, optimization, and stakeholder planning.
+- Use `ftk-database-query` instead when the main deliverable is a specific KQL query, schema explanation, or detailed Hub database result.
+- Use `chief-financial-officer` instead when the main deliverable is an executive finance narrative, budget decision, board-ready summary, or capital-allocation recommendation.
+- Use `azure-capacity-manager` instead when the constraint is quota, capacity reservation supply, region access, zones, or AKS capacity readiness.
+- Use `ftk-hubs-agent` instead when the work is deploying, upgrading, configuring, or troubleshooting a FinOps hub.
+
+
+
+## azure-capacity-manager
+
+Domain: Azure capacity evidence for Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, and Automation, Tools & Services.
+
+The `azure-capacity-manager` agent maps Azure quota, region, SKU, zone, capacity reservation, and AKS capacity evidence into the FinOps Framework. It separates capacity guarantees from pricing commitments so teams can make better quota, reservation, and savings decisions.
+
+What it does:
+
+- Reviews quota usage, quota increases, quota groups, transfers, and headroom.
+- Plans capacity reservation groups, sharing, overallocation, and utilization checks.
+- Evaluates region access, zonal enablement, and logical-to-physical zone alignment.
+- Checks AKS node pool capacity readiness and non-compute quota constraints.
+- Aligns capacity planning with reservation and savings plan recommendations.
+
+Key tools it uses:
+
+- Azure CLI tools, such as `RunAzCliReadCommands`, `RunAzCliWriteCommands`, and `GetAzCliHelp`
+- Python analysis through `execute_python`
+- Capacity tools such as `vm-quota-usage`, `non-compute-quotas`, `db-service-quotas`, `sku-availability`, and `capacity-reservation-groups`
+- Azure Resource Graph and benefit recommendation tools where capacity, inventory, and rate-optimization context intersect
+- Delegation to `ftk-database-query` for Kusto-backed forecast, commitment utilization, savings, pricing, recommendation, and transaction evidence
+
+When it's invoked:
+
+- A user asks about quota, capacity reservations, region access, availability zones, AKS capacity, or capacity governance.
+- A scheduled capacity task checks daily quota usage, weekly capacity readiness, monthly planning, or quarterly strategy.
+- Another agent needs capacity context before recommending commitments, scaling changes, or regional moves.
+
+Example prompts:
+
+- "Check whether we have enough quota and regional capacity for next month's AKS node pool expansion."
+- "Plan a capacity reservation strategy for a production workload that needs guaranteed VM supply in two regions."
+- "Compare our capacity reservation utilization with savings plan recommendations before we buy more commitments."
+- "Explain the zone mapping risks before we move this workload to a multi-zone architecture."
+
+Expected output:
+
+- A capacity-readiness assessment with quota, region, zone, reservation, and utilization findings.
+- Recommended capacity actions, such as quota requests, reservation group changes, region alternatives, or monitoring tasks.
+- Explicit distinction between capacity guarantees and pricing commitments so teams don't treat reservations, savings plans, and quota as interchangeable.
+
+Use this agent versus another:
+
+- Use `azure-capacity-manager` when supply availability is the main question: quota, capacity reservations, region access, zones, AKS node pools, or capacity governance.
+- Use `finops-practitioner` instead when the question is primarily about FinOps operating model, allocation, anomaly response, or broad cost optimization.
+- Use `chief-financial-officer` instead when capacity decisions need executive budget framing, investment trade-offs, or board-ready risk language.
+- Use `ftk-database-query` instead when you need exact Hub Kusto results about cost, utilization, recommendations, or pricing before making a capacity decision.
+- Use `ftk-hubs-agent` instead when the capacity issue is blocking a hub deployment, upgrade, or configuration workflow.
+
+
+
+## chief-financial-officer
+
+Domain: Executive finance, budgeting, forecasting, risk, capital allocation, cloud economics, and board-ready FinOps narratives.
+
+The `chief-financial-officer` agent turns evidence packages into financial guidance. It focuses on business outcomes, executive summaries, quantified assumptions, and decision-ready recommendations. It is a consultative persona and doesn't own autonomous scheduled tasks or raw data collection.
+
+What it does:
+
+- Prepares budgeting, rolling forecasts, variance analysis, and KPI-driven performance views.
+- Evaluates investment priorities, capital allocation, risk, controls, and compliance exposure.
+- Assesses cloud spend through unit economics, commitment decisions, and value creation.
+- Produces recommendations for boards, CEOs, finance leaders, and FinOps stakeholders.
+- Calls out missing data, uncertainty, risks, and follow-up steps.
+
+Key tools it uses:
+
+- Python analysis through `execute_python` for scenario calculations and executive packaging
+- Evidence packages from `finops-practitioner`, `ftk-database-query`, and `azure-capacity-manager`
+- Teams tools for final communication when a configured workflow asks for executive delivery
+
+When it's invoked:
+
+- A user asks for executive summaries, budget variance, forecast drift, board narratives, or portfolio trade-offs.
+- The `finops-practitioner` agent needs finance leadership for executive framing, prioritization, commitment risk, or investment tradeoffs.
+
+Example prompts:
+
+- "Summarize cloud spend performance for the CFO with risks, opportunities, and decisions needed this week."
+- "Explain the budget variance for the quarter and separate timing issues from structural run-rate changes."
+- "Create a board-ready narrative for our AI investment, including unit economics and governance risks."
+- "Prioritize these three optimization programs by financial impact, execution risk, and business value."
+
+Expected output:
+
+- An executive-ready finance view with quantified assumptions, variance explanations, risks, options, and recommended decisions.
+- Budget, forecast, portfolio, or investment guidance that connects cloud spend to business outcomes.
+- Clear caveats for missing evidence, confidence level, and follow-up data needed before committing to a decision.
+
+Use this agent versus another:
+
+- Use `chief-financial-officer` when the audience is executives, finance leaders, boards, or business owners making budget, forecast, risk, or investment decisions.
+- Use `finops-practitioner` instead when the work needs broader FinOps process guidance, practitioner playbooks, or operating-model design.
+- Use `ftk-database-query` instead when the deliverable is detailed KQL, data extraction, schema validation, pricing lookup, or recommendation analysis.
+- Use `azure-capacity-manager` instead when the core issue is capacity supply, quota, zones, reservation groups, or regional access.
+- Use `ftk-hubs-agent` instead when financial reporting is blocked by hub deployment, export, connectivity, version, or health issues.
+
+
+
+## ftk-database-query
+
+Domain: FinOps hub database analysis, FOCUS-aligned KQL, schema validation, query diagnostics, pricing, recommendations, and transactions.
+
+The `ftk-database-query` agent is the KQL specialist. It queries the Hub database, explains schema choices, and adapts catalog queries before writing custom KQL.
+
+What it does:
+
+- Uses the Hub database for analytics and avoids the Ingestion database for end-user analysis.
+- Starts with the query catalog and adapts the closest existing query when possible.
+- Builds scoped custom drill-downs from `costs-enriched-base` when no catalog query fits and uses aggregate tools for full-month or scheduled reports.
+- Uses `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` for FOCUS-aligned analysis.
+- Explains filters, functions, metrics, missing data, and data freshness concerns.
+
+Key tools it uses:
+
+- Memory and Azure discovery through `SearchMemory`, `RunAzCliReadCommands`, and `GetAzCliHelp`
+- Python analysis through `execute_python`
+- The full FinOps hub Kusto catalog, including `costs-enriched-base`, `monthly-cost-trend`, `top-services-by-cost`, `cost-by-region-trend`, `cost-anomaly-detection`, `cost-forecasting-model`, `commitment-discount-utilization`, `savings-summary-report`, `top-other-transactions`, and AI cost tools
+
+When it's invoked:
+
+- A user asks for a KQL query, schema explanation, pricing lookup, recommendation analysis, transaction analysis, or cost drill-down.
+- A scheduled task needs exact FinOps hub data, a focused diagnostic, or FOCUS-aligned query execution.
+- Another agent needs live cost data or schema-aware analysis before making a recommendation.
+
+Example prompts:
+
+- "Write a FOCUS-aligned KQL query that shows month-over-month cost by service and billing account."
+- "Use the Hub database to find the top resources driving last week's cost anomaly."
+- "Explain which columns from `Costs()` I should use for amortized cost, list cost, and chargeback reporting."
+- "Compare prices for this SKU across regions and show the assumptions in the query."
+
+Expected output:
+
+- A query-first answer with the selected catalog query or custom KQL, filters, assumptions, and expected columns.
+- Data interpretation that explains schema choices, functions, time windows, aggregation grain, and freshness limitations.
+- Diagnostics for query failures or missing results, including what to validate in the Hub database or source exports.
+
+Use this agent versus another:
+
+- Use `ftk-database-query` when the question depends on exact FinOps hub data, KQL, schema details, pricing data, recommendations, or transaction records.
+- Use `finops-practitioner` instead when you need to turn query results into a broader FinOps plan, operating-model recommendation, or governance approach.
+- Use `chief-financial-officer` instead when data results need to become an executive budget, forecast, risk, or portfolio decision narrative.
+- Use `azure-capacity-manager` instead when the query is only supporting quota, capacity reservation, regional supply, zone, or AKS readiness decisions.
+- Use `ftk-hubs-agent` instead when the problem is that hub data is missing, stale, inaccessible, or caused by deployment or export health issues.
+
+
+
+## ftk-hubs-agent
+
+Domain: FinOps hubs deployment, upgrades, configuration, exports, dashboards, connectivity, and health.
+
+The `ftk-hubs-agent` agent keeps FinOps hubs running. It uses validation-first workflows for deployment, upgrade, maintenance, and troubleshooting.
+
+What it does:
+
+- Deploys and upgrades FinOps hubs.
+- Maintains hub configuration, exports, dashboards, and connectivity.
+- Troubleshoots deployment failures and unhealthy hub environments.
+- Checks prerequisites, required resource providers, RBAC, regions, quotas, naming conflicts, and existing resources.
+- Runs template validation and what-if checks before deploy or upgrade changes.
+
+Key tools it uses:
+
+- Knowledge search through `SearchMemory`
+- Azure CLI tools, such as `RunAzCliReadCommands`, `RunAzCliWriteCommands`, and `GetAzCliHelp`
+- Direct REST freshness checks through `data-freshness-check`, which treats `Costs()` as the source of truth for hub data freshness
+
+When it's invoked:
+
+- A user asks to deploy, upgrade, configure, maintain, or troubleshoot FinOps hubs.
+- A scheduled health task checks FinOps hub readiness, export freshness, version status, or connectivity.
+- Another agent needs hub health context before trusting query results or scheduled reports.
+
+Example prompts:
+
+- "Validate whether my environment is ready to deploy FinOps hubs and list any blocking prerequisites."
+- "Troubleshoot why my Cost Management exports are not showing up in the Hub database."
+- "Plan a safe FinOps hubs upgrade and include validation, what-if, and rollback checks."
+- "Check hub data freshness and explain whether reports should trust the current `Costs()` results."
+
+Expected output:
+
+- A deployment, upgrade, configuration, or troubleshooting plan that starts with validation and identifies blockers before changes.
+- Hub health findings for prerequisites, RBAC, providers, exports, connectivity, version status, and data freshness.
+- Recommended next actions, including safe validation commands, what-if checks, and escalation points when write operations are required.
+
+Use this agent versus another:
+
+- Use `ftk-hubs-agent` when the work changes or diagnoses the FinOps hub platform itself: deployment, upgrade, configuration, exports, connectivity, dashboards, or health.
+- Use `ftk-database-query` instead when the hub is healthy and the question is about KQL, schema, pricing, recommendations, transactions, or cost drill-downs.
+- Use `finops-practitioner` instead when hub data needs to become a FinOps process, optimization, allocation, governance, or anomaly response recommendation.
+- Use `chief-financial-officer` instead when hub findings need executive finance framing, budget guidance, or portfolio prioritization.
+- Use `azure-capacity-manager` instead when a deployment or scaling issue is primarily about Azure quota, capacity reservation supply, region access, or zones.
+
+
+
+## Handoff model
+
+The orchestrator starts with the user's prompt or scheduled task instructions. It selects the agent whose `handoffDescription` best matches the task, then passes the conversation to that specialist.
+
+Agents can also recommend handoffs when the work crosses domain boundaries:
+
+- `finops-practitioner` hands executive finance narratives, portfolio prioritization, and board-level recommendations to `chief-financial-officer`.
+- `finops-practitioner` hands deep FinOps hub database and KQL work to `ftk-database-query`.
+- `finops-practitioner` hands capacity evidence requests to `azure-capacity-manager` when the task involves quota, capacity reservations, region access, zones, AKS capacity, or capacity governance.
+- Hub deployment, upgrade, and troubleshooting work routes to `ftk-hubs-agent`.
+
+Handoffs keep each response focused. The FinOps specialist can frame the business problem, the KQL specialist can gather evidence, the capacity specialist can validate supply constraints, and the finance specialist can turn the findings into an executive decision.
+
+
+
+## Skills
+
+Skills give agents a domain reference map. The template applies all three skills to Azure SRE Agent. When a prompt or scheduled task enters one of these domains, the matching skill can load before the agent responds.
+
+### azure-capacity-management
+
+Provides Azure capacity management guidance for SaaS ISVs running workloads in their own Azure subscriptions under Enterprise Agreement or Microsoft Customer Agreement billing. It covers quota operations, quota groups, capacity reservation groups, region access, zonal enablement, AKS capacity governance, non-compute quotas, and capacity alerts. In this recipe, those references are used as implementation detail under the canonical FinOps Framework capabilities.
+
+The `azure-capacity-manager` agent is instructed to load this skill before capacity work. Capacity scheduled tasks also tell the agent to load it before checking quota, capacity reservation, SKU, or supply-readiness signals.
+
+### azure-cost-management
+
+Provides Azure cost optimization and financial governance guidance. It covers Advisor recommendations, commitment discounts, budgets, exports, anomaly alerts, credits, Microsoft Azure Consumption Commitment tracking, orphaned resources, VM rightsizing, and Azure retail price lookup.
+
+FinOps and reporting scheduled tasks load this skill with the `finops-toolkit` skill when they need Microsoft Cost Management context beyond FinOps hub Kusto data.
+
+### finops-toolkit
+
+Provides FinOps toolkit and FinOps hubs guidance. It maps tasks to the right references, explains Hub database functions, lists the Kusto query catalog, and covers hub deployment, upgrade, health checks, FOCUS mapping, Power BI, alerts, workbooks, and the FinOps toolkit for PowerShell.
+
+FinOps hub query, reporting, and AI cost tasks load this skill so agents can use the Hub database, `Costs()`, `Prices()`, `Recommendations()`, `Transactions()`, and the predefined Kusto query catalog correctly.
+
+
+
+## How agents, tools, skills, and knowledge work together
+
+The agent combines four layers:
+
+1. **Agents** route the work to the right specialist.
+2. **Skills** load domain guidance and reference maps.
+3. **Tools** gather evidence from Azure, FinOps hubs, Python analysis, and Kusto queries.
+4. **Knowledge** supplies onboarding guidance, notification patterns, and known issue context.
+
+Together, these layers help the agent move from a question to an evidence-backed recommendation. The orchestrator delegates, the specialist loads the right skill, tools collect the data, and knowledge keeps the answer aligned with the deployed environment. The FinOps practitioner coordinates the operating model, the database specialist gathers Kusto evidence, the capacity specialist gathers Azure capacity evidence, and the CFO supplies finance and leadership framing.
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Cost allocation](../../framework/understand/allocation.md)
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Architecting for cloud](../../framework/optimize/architecting.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+- [Usage optimization](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Azure Monitor](/azure/azure-monitor/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [FinOps hubs](../hubs/finops-hubs-overview.md)
+- [Azure SRE Agent template reference (FinOps toolkit)](template.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/deploy.md b/docs-mslearn/toolkit/sre-agent/deploy.md
new file mode 100644
index 000000000..86269d484
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/deploy.md
@@ -0,0 +1,215 @@
+---
+title: Deploy Azure SRE Agent with the FinOps toolkit
+description: Deploy the FinOps toolkit Azure SRE Agent template from the Azure portal or CLI, connect it to a FinOps hub Data Explorer cluster, and validate the deployment.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/17/2026
+ms.topic: tutorial
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps hub admin, I want to deploy and configure the FinOps toolkit's Azure SRE Agent so that I can receive scheduled cost reports, anomaly detection, and capacity monitoring.
+---
+
+
+
+# Deploy Azure SRE Agent with the FinOps toolkit
+
+In this tutorial, you learn how to deploy the [FinOps toolkit Azure SRE Agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent), connect it to a [FinOps hub](../hubs/finops-hubs-overview.md), and validate the deployment.
+
+The deployment flow is copied from the Microsoft SRE Agent starter lab and updated for the FinOps toolkit. The Azure portal path uses the toolkit's packaged ARM template and an embedded deployment script to apply the SRE Agent recipe. The local CLI path uses Azure CLI + Bicep for infrastructure and the supported SRE Agent ARM and data-plane surfaces for recipe configuration. It doesn't use `azd`.
+
+## What gets deployed
+
+The FinOps hub recipe (`src/templates/sre-agent/recipes/finops-hub/`) deploys:
+
+| Component | Count | Notes |
+|-----------|-------|-------|
+| SRE Agent | 1 | `Microsoft.App/agents` |
+| Model provider | 1 | Azure OpenAI provider-level routing (`MicrosoftFoundry` ARM value, `Automatic` model routing) |
+| Managed identities | 1-2 | Agent system-assigned managed identity; portal deployments also create a user-assigned identity for the deployment script |
+| Log Analytics workspace | 1 | Linked to the agent for telemetry |
+| Application Insights | 1 | Linked to Log Analytics |
+| Custom agents | 5 | FinOps practitioner orchestrator plus CFO, capacity, database-query, and hubs specialists |
+| Skills | 3 | Capacity, cost management, and FinOps Toolkit |
+| Tools | 50 | Kusto, capacity, and Hub infrastructure tools |
+| Tool overrides | 9 | Enables SRE Agent Log Query and Visualization tools |
+| Scheduled tasks | 19 | FinOps, governance, and reporting automations |
+| Kusto connector | 0 or 1 | Included when you pass `--cluster-uri` |
+| Knowledge docs | 6 | Five recipe knowledge docs plus the FinOps Toolkit output style |
+
+## Prerequisites
+
+- A deployed FinOps hub with Data Explorer.
+- Permissions to create deployed resources, such as **Contributor** on the subscription when the template creates the agent resource group.
+- Permissions to assign roles at subscription, target resource group, and agent scopes, such as **Role Based Access Control Administrator**, **User Access Administrator**, or **Owner** on those scopes.
+- The `Microsoft.App` resource provider registered in the subscription.
+- For local CLI deployments only: [Azure CLI](/cli/azure/install-azure-cli), `curl`, `jq`, `python3` with `PyYAML`, and Bash 3.2 or newer.
+
+Run:
+
+```bash
+cd src/templates/sre-agent
+bash bin/check-prerequisites.sh --subscription
+```
+
+## Deploy the FinOps hub recipe
+
+Use the Azure portal deployment when you want the same one-click experience as other FinOps toolkit templates. The template creates the Azure resources, grants the deployment script identity SRE Agent Administrator on the agent, downloads the packaged recipe assets, and applies connectors, tools, skills, subagents, knowledge, and scheduled tasks.
+
+
+> [!div class="nextstepaction"]
+> [Deploy to Azure](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fmicrosoft.github.io%2Ffinops-toolkit%2Fdeploy%2Fsre-agent%2Flatest%2FcreateUiDefinition.json)
+
+
+### Deploy from local CLI
+
+Run one script with explicit parameters:
+
+```bash
+cd src/templates/sre-agent
+
+bash bin/deploy.sh \
+ --recipe recipes/finops-hub \
+ --subscription \
+ --resource-group \
+ --name \
+ --location \
+ --cluster-uri https://..kusto.windows.net/Hub \
+ [--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] \
+ [--target-resource-group ...] \
+ [--dry-run] \
+ [--force]
+```
+
+`bin/deploy.sh --help` is the CLI contract:
+
+```text
+Usage: bash bin/deploy.sh --recipe [options]
+
+Required:
+ --recipe Recipe directory
+ --subscription Azure subscription
+ -g, --resource-group Resource group for the agent
+ -n, --name Agent name
+ -l, --location Azure region
+
+Optional:
+ --target-resource-group Repeatable target resource group. Defaults to --resource-group.
+ --cluster-uri Kusto connector URI, including database name.
+ Example: https://..kusto.windows.net/Hub
+ --cluster-resource-id Optional Kusto cluster ARM resource ID. Real deployments resolve this from --cluster-uri when possible; dry-run requires it.
+ --no-subscription-reader Do not assign Reader at subscription scope. Default: no subscription Reader (opt-in with --subscription-reader).
+ --deploy-name Deployment name override. Defaults to a deterministic name.
+ --dry-run Validate inputs and write parameters without Azure calls.
+ --force Accepted for compatibility.
+ -h, --help Show this help.
+```
+
+## Validation modes
+
+Dry-run is hermetic and skips live Azure calls:
+
+```bash
+bash bin/deploy.sh \
+ --recipe recipes/finops-hub \
+ --subscription \
+ -g \
+ -n \
+ -l \
+ --cluster-uri https://..kusto.windows.net/Hub \
+ --cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/ \
+ --dry-run
+```
+
+When deploying from the portal, the template runs a subscription-scoped ARM deployment and then runs a `Microsoft.Resources/deploymentScripts` resource to configure the Kusto connector and remaining recipe assets through the supported SRE Agent ARM and data-plane surfaces. When deploying locally, `deploy.sh` runs the same subscription-scoped infrastructure deployment and then runs `bin/apply-extras.sh` for the recipe configuration step.
+
+The recipe defaults the parent SRE Agent to Azure OpenAI provider-level routing by setting `defaultModel.provider` to `MicrosoftFoundry` and `defaultModel.name` to `Automatic`. Azure SRE Agent automatically selects the model within the configured provider for each task. The template doesn't pin different models for individual custom agents or scheduled tasks because the documented SRE Agent configuration surface is provider-level.
+
+Resource names are deterministic for the subscription ID, agent resource group ID, and agent name. Use the same values to update an existing deployment. Post-provisioning deletes existing scheduled tasks with the recipe's task names before applying manifests so redeployments don't create duplicate automations. `--deploy-name` only changes the ARM deployment record and local build directory.
+
+The agent defaults to Low (read-only) access level for reporting and analysis without modification risk. Subscription Reader is opt-in (`--subscription-reader`) and only required for subscription-wide ARM-backed reports; otherwise the agent scopes reads via target resource groups. This posture allows the 19 scheduled reports to run autonomously while eliminating write blast radius.
+
+If `--cluster-uri` points to an Azure Data Explorer cluster with `publicNetworkAccess` set to `Disabled`, the script still deploys all resources, assigns the agent identity `AllDatabasesViewer`, and creates the `finops-hub-kusto` connector. It also prints a warning with the SRE Agent known-limitations URL because private endpoint ADX blocks direct KQL queries from the hosted agent. The customer can decide whether to enable public query access for the connector after reviewing .
+
+## Recipe identity defaults
+
+- Shipped recipes in this repo omit the `identity` block.
+- Customer-authored recipes can keep identity defaults in `agent.json`.
+- CLI flags always override recipe defaults.
+
+If you omit `--target-resource-group`, the deploy flow uses the recipe default when present; otherwise it falls back to the agent resource group.
+
+## Verify the deployment
+
+```bash
+bash bin/verify-agent.sh \
+ $(az account show --query id -o tsv) \
+ \
+ \
+ --expected recipes/finops-hub
+```
+
+Then confirm the agent in [sre.azure.com](https://sre.azure.com). If you passed `--cluster-uri`, verify the `finops-hub-kusto` connector. If the deployment warned that the cluster denies public query access, the connector is expected to remain unhealthy until the customer enables public query access or Microsoft adds private endpoint ADX query support for SRE Agent.
+
+## Configure notifications
+
+Scheduled tasks deliver reports to Microsoft Teams and Outlook through Azure SRE Agent notification connectors. Connectors require interactive OAuth setup in [sre.azure.com](https://sre.azure.com), so the portal template and `bin/deploy.sh` don't create them.
+
+### Configure Teams
+
+1. Open [sre.azure.com](https://sre.azure.com), open your agent, then go to **Builder** > **Connectors**.
+2. Select **Add connector** > **Send notification (Microsoft Teams)**.
+3. Sign in with your Microsoft 365 account.
+4. Paste the channel URL from **Get link to channel** in Teams.
+5. Select the agent's managed identity and save.
+6. Test from chat: `Post a test message to our Teams channel saying "Azure SRE Agent connected via the FinOps toolkit."`
+
+Use the built-in `PostTeamsMessage` tool from the [Teams notification guidance](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/recipes/finops-hub/knowledge/teams-notification-guide.md). Don't call the Microsoft Graph API or the connection's `dynamicInvoke` endpoint directly because that path returns a 403 error for this connector configuration.
+
+### Configure Outlook
+
+1. Open [sre.azure.com](https://sre.azure.com), open your agent, then go to **Builder** > **Connectors**.
+2. Select **Add connector** > **Outlook Tools (Office 365 Outlook)**.
+3. Sign in with a Microsoft 365 account that has mailbox access.
+4. Select the agent's managed identity and save.
+5. Test from chat: `Send an email to with subject "SRE Agent test" and body "Outlook connector is working."`
+
+For more information, see [Send notifications in Azure SRE Agent](/azure/sre-agent/send-notifications).
+
+## GitHub Actions example
+
+The included GitHub Actions example passes the cluster URI as a deploy flag while leaving secret-only inputs such as `GITHUB_PAT` and `ADO_PAT` in the environment.
+
+## Migrating from env-var-driven deploys
+
+The old deploy flow used configuration inputs such as `FINOPS_HUB_CLUSTER_URI`, `FINOPS_HUB_CLUSTER_RESOURCE_ID`, and `connectors.secrets.env`. Those inputs are no longer supported for identity or config.
+
+Before:
+
+```bash
+export FINOPS_HUB_CLUSTER_URI="https://..kusto.windows.net/hub"
+export FINOPS_HUB_CLUSTER_RESOURCE_ID="/subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/"
+bash bin/deploy.sh recipes/finops-hub
+```
+
+After:
+
+```bash
+bash bin/deploy.sh \
+ --recipe recipes/finops-hub \
+ --subscription \
+ --resource-group \
+ --name \
+ --location \
+ --cluster-uri https://..kusto.windows.net/Hub \
+ [--cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/]
+```
+
+The only supported environment-variable inputs are secrets:
+
+- `GITHUB_PAT`
+- `ADO_PAT`
+- `ADO_USE_AAD`
+- `ADO_USE_MI`
+- `ADO_ORG`
diff --git a/docs-mslearn/toolkit/sre-agent/get-started.md b/docs-mslearn/toolkit/sre-agent/get-started.md
new file mode 100644
index 000000000..eefc72449
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/get-started.md
@@ -0,0 +1,135 @@
+---
+title: Get started with the FinOps toolkit on Azure SRE Agent
+description: Learn what to do after deploying Azure SRE Agent with the FinOps toolkit — first queries, scheduled tasks, and specialized subagents.
+author: flanakin
+ms.author: micflan
+ms.date: 06/04/2026
+ms.topic: quickstart
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: arclares
+# customer intent: As a FinOps practitioner, I want to know what to do after deploying the FinOps toolkit's Azure SRE Agent so I can start getting value from it immediately.
+---
+
+# Get started with the FinOps toolkit on Azure SRE Agent
+
+You've deployed [Azure SRE Agent with the FinOps toolkit](overview.md). Here's how to start using it after the [deployment workflow](deploy.md) finishes.
+
+The [deployment guide](deploy.md) covers how to deploy and configure the agent. This guide focuses on what to do next: post-deployment prompts, [scheduled tasks](scheduled-tasks.md), [specialist agents](agents.md), and customization.
+
+
+
+## Talk to the agent
+
+Azure SRE Agent responds to natural language questions about your Azure environment. The [tool catalog](tools.md) describes how Kusto tools query [FinOps hubs](../hubs/finops-hubs-overview.md) data, while Python tools call Azure APIs.
+
+Ask questions the same way you'd ask another FinOps or platform engineer. The [agent and skills reference](agents.md) explains how the orchestrator routes work to specialist agents that use tools, skills, and knowledge to ground recommendations.
+
+
+
+## Automation map
+
+Use this map to connect each post-deployment activity to the agent, task, tool, output, and decision pattern. The [Azure SRE Agent overview](overview.md), [agents reference](agents.md), [tools reference](tools.md), and [scheduled tasks reference](scheduled-tasks.md) document the full catalog.
+
+| Capability | Agent | Tasks | Tools | Output | You decide |
+|------------|-------|-------|-------|--------|------------|
+| Cost visibility and anomaly review | [`finops-practitioner`](agents.md#finops-practitioner) | [`Monthly`](scheduled-tasks.md#monthly), [`CostOptimization`](scheduled-tasks.md#costoptimization), and [`AlertCoverageAudit`](scheduled-tasks.md#alertcoverageaudit) | [Cost analysis](tools.md#cost-analysis-and-reporting), [anomaly detection](tools.md#anomaly-detection-and-forecasting), [forecasting](tools.md#anomaly-detection-and-forecasting), and [rate optimization](tools.md#rate-optimization) tools through [`ftk-database-query`](agents.md#ftk-database-query) where Kusto evidence is required | Cost drivers, anomaly findings, forecasts, and prioritized actions from the scheduled task outputs | Which owners investigate, which anomalies are expected, and which optimization actions should move first |
+| Capacity and quota posture | [`azure-capacity-manager`](agents.md#azure-capacity-manager) | [`CapacityDailyMonitor`](scheduled-tasks.md#capacitydailymonitor), [`ComputeUtilizationTrend`](scheduled-tasks.md#computeutilizationtrend), [`CapacityWeeklySupplyReview`](scheduled-tasks.md#capacityweeklysupplyreview), and [`NonComputeQuotaAudit`](scheduled-tasks.md#noncomputequotaaudit) | [`vm-quota-usage`](tools.md#capacity-management), [`capacity-reservation-groups`](tools.md#capacity-management), [`non-compute-quotas`](tools.md#capacity-management), and [`sku-availability`](tools.md#capacity-management) | Quota pressure, capacity reservation waste, SKU restrictions, and capacity blockers from the capacity task outputs | Which quota requests, region changes, reservation changes, or workload moves need action |
+| Finance and commitment decisions | [`finops-practitioner`](agents.md#finops-practitioner), consulting [`chief-financial-officer`](agents.md#chief-financial-officer) | [`BenefitRecommendationReview`](scheduled-tasks.md#benefitrecommendationreview), [`Semiannual`](scheduled-tasks.md#semiannual), and [`AIWorkloadCostAnalysis`](scheduled-tasks.md#aiworkloadcostanalysis) | [Benefit, savings, commitment, forecasting, and AI cost tools](tools.md#rate-optimization) through [`ftk-database-query`](agents.md#ftk-database-query) where Kusto evidence is required | Executive summaries, savings opportunities, forecast risk, and decision-ready commitment context from FinOps task outputs with CFO framing | Which purchases, deferrals, budget changes, or executive escalations are approved |
+| Hub health and data trust | [`ftk-hubs-agent`](agents.md#ftk-hubs-agent) | [`HubsHealthCheck`](scheduled-tasks.md#hubshealthcheck) and [`MonitoringScopeValidation`](scheduled-tasks.md#monitoringscopevalidation) | [`data-freshness-check`](tools.md#data-ingestion-and-health), Azure discovery, and hub configuration tools | Hub version, connectivity, data freshness, and monitoring coverage findings from hub health task outputs | Whether reports are trustworthy, which exports need repair, and when to pause analysis that depends on stale data |
+
+
+
+## Getting started: first things to try
+
+Start with focused questions that validate your data, summarize spend, and find common optimization opportunities that map to the documented [tool catalog](tools.md) and [scheduled-task coverage](scheduled-tasks.md#task-details).
+
+1. Check your data pipeline and scheduled outputs: "Is my FinOps hubs data fresh, which [scheduled tasks](scheduled-tasks.md#task-details) ran recently, and what reports or Teams notifications did they produce?"
+2. Review your spending and waste: "What did we spend last month by subscription, and which idle VMs across subscriptions should we investigate first?"
+3. Review anomalies: "Show me this week's cost anomalies, explain the likely drivers, and recommend who should investigate each one."
+4. Find savings: "What reservation recommendations do I have?"
+5. Check capacity posture: "What's my quota utilization in eastus?"
+
+If a response doesn't have enough context, narrow the question by subscription, management group, billing scope, region, resource type, or reporting period so the agent can select focused tools and filters from the [tool catalog](tools.md).
+
+
+
+## Scheduled tasks run automatically
+
+The agent runs [19 scheduled tasks](scheduled-tasks.md) on daily, weekly, monthly, semiannual, and quarterly cadences. The [scheduled tasks reference](scheduled-tasks.md) lists daily, weekly, monthly, semiannual, and quarterly tasks for health, optimization, capacity, planning, finance, and strategy reviews.
+
+You don't need to trigger scheduled tasks because each task has a cron expression in its task definition, and [task details](scheduled-tasks.md#task-details) describe how reports are formatted for Microsoft Teams when notifications are configured.
+
+Customize the defaults before you rely on the automation in production. Review each scheduled task's `cron_expression` and prompt thresholds in the [task details](scheduled-tasks.md#task-details), then adjust schedules and thresholds to match your operating rhythm, time zone, reporting calendar, and risk tolerance.
+
+For example, move the month-over-month report to run after billing data settles. Lower quota headroom thresholds for regions with known capacity pressure. Raise anomaly review thresholds for subscriptions with expected seasonal spikes. The [customization options](scheduled-tasks.md#task-details) cover each task in detail.
+
+
+
+## Build on the basics
+
+The agent includes [5 specialized subagents](agents.md), and you can ask the orchestrator to route the work or mention the specialist when you know which domain you need.
+
+- [`finops-practitioner`](agents.md#finops-practitioner) — cost analysis, optimization, budgets, alerts, and FinOps practice guidance
+- [`azure-capacity-manager`](agents.md#azure-capacity-manager) — quota, capacity reservations, SKU availability, and capacity governance
+- [`chief-financial-officer`](agents.md#chief-financial-officer) — consultative financial strategy, commitment decisions, budgeting, forecasting, and executive finance narratives
+- [`ftk-database-query`](agents.md#ftk-database-query) — direct Kusto queries, schema validation, pricing, recommendations, and transactions against FinOps hubs
+- [`ftk-hubs-agent`](agents.md#ftk-hubs-agent) — hub health, data freshness, exports, connectivity, deployment, and upgrade troubleshooting
+
+Use specialist names when you want a specific lens. For example, ask the [`chief-financial-officer`](agents.md#chief-financial-officer) agent to frame a commitment decision for leadership, or ask the [`ftk-database-query`](agents.md#ftk-database-query) agent to explain the KQL behind a cost trend.
+
+
+
+## Learn more
+
+Use these pages as your next steps:
+
+- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md)
+- [Specialist agents and skills](agents.md)
+- [Scheduled tasks (Azure SRE Agent in the FinOps toolkit)](scheduled-tasks.md)
+- [FinOps toolkit best practices](../../best-practices/library.md)
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/GetStarted)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Architecting for cloud](../../framework/optimize/architecting.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+- [Usage optimization](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Microsoft Cost Management](/azure/cost-management-billing/costs/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md)
+- [Specialist agents and skills](agents.md)
+- [Scheduled tasks (Azure SRE Agent in the FinOps toolkit)](scheduled-tasks.md)
+- [FinOps toolkit best practices](../../best-practices/library.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/knowledge.md b/docs-mslearn/toolkit/sre-agent/knowledge.md
new file mode 100644
index 000000000..985a92b4f
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/knowledge.md
@@ -0,0 +1,172 @@
+---
+title: Manage knowledge and memory (Azure SRE Agent in the FinOps toolkit)
+description: Learn how knowledge grounds the FinOps toolkit's Azure SRE Agent in your team's context and how memory keeps operational learnings available across sessions and redeployments.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: concept-article
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to manage knowledge and memory for Azure SRE Agent so that the agent can answer with my team's operational context.
+---
+
+# Manage knowledge and memory (Azure SRE Agent in the FinOps toolkit)
+
+Knowledge grounds the agent in your team's context. It helps the agent answer with your runbooks, known issues, notification patterns, and deployment details instead of relying on general guidance alone.
+
+Use knowledge and memory together:
+
+- **Knowledge** gives the agent trusted reference material, such as runbooks, architecture notes, and connector setup guidance.
+- **Memory** keeps useful learnings from investigations, user instructions, and recurring operational patterns.
+- **Session insights** help the agent learn from previous work, including what fixed an issue and what did not.
+
+
+
+## Shipped knowledge docs
+
+The [Azure SRE Agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent) uploads six knowledge documents during extras configuration. Five are under `recipes/finops-hub/knowledge/`; the shared FinOps Toolkit output style is uploaded from `src/templates/claude-plugin/output-styles/ftk-output-style.md`. The script uploads them as portal-visible Knowledge Sources and verifies the KnowledgeFile sources are indexed before continuing so missing or unindexed documents fail deployment instead of becoming a silent runtime issue.
+
+| Knowledge doc | What it provides |
+|---|---|
+| `chart-artifact-verification.md` | Guidance for validating generated charts, tables, and downloadable artifacts before sending reports to stakeholders. |
+| `document-index.md` | Index of the shipped FinOps, capacity, and hub-operation documents the agent can use for grounding. |
+| `ftk-output-style.md` | Shared report style for evidence-backed financial and capacity-management output, including FinOps capability sections, thresholds, confidence, caveats, and disclaimers. |
+| `onboarding-recommendations.md` | First-run guidance for team onboarding, `/learn`, and "What should I do next?" prompts. It reminds the agent to validate Azure access, enable visualization tools, configure FinOps hub data sources, and recommend Outlook and Microsoft Teams connectors when needed. |
+| `teams-notification-guide.md` | Delivery guidance for scheduled reports and notifications. It tells the agent to use the built-in `PostTeamsMessage` and `ReplyToTeamsThread` tools, format Teams messages as HTML, and avoid unsupported direct calls to Microsoft Graph or connector endpoints. |
+| `known-issues-and-workarounds.md` | Operational workarounds found during scheduled task validation. It covers stale data detection, Resource Graph fallbacks, quota command issues, JMESPath escaping, memory write conflicts, Kusto query errors, and the split between financial data in Teams and operational learnings in memory. |
+
+> [!TIP]
+> Keep these docs short and practical. The agent uses them best when each file gives clear instructions, constraints, and examples.
+
+
+
+## Memory system
+
+Azure SRE Agent uses memory to carry context forward between threads. The agent extends that with these memory layers to improve recommendations over time.
+
+### Session insights
+
+Session insights are extracted automatically after a thread goes quiet. They capture useful details from the conversation, such as:
+
+- Symptoms observed
+- Resolution steps
+- Root cause
+- Pitfalls to avoid
+
+For FinOps operations, this helps the agent remember patterns like stale export data, commands that failed, fallbacks that worked, and report delivery issues. Session insights are most useful when users correct or rate responses after an investigation.
+
+### User memories
+
+User memories are explicit facts that you ask the agent to save. Use them for stable details the agent should remember, such as team preferences, environment constraints, or recurring operational notes.
+
+Use these commands in chat:
+
+- `#remember` to save a fact
+- `#retrieve` to find saved facts
+- `#forget` to delete a saved fact
+
+> [!IMPORTANT]
+> Save operational context, not sensitive financial figures. Send costs, savings, forecasts, and grade results to Teams reports instead of persisting them in memory.
+
+### Proactive knowledge persistence
+
+The agent also maintains synthesized knowledge files under `memories/synthesizedKnowledge/`.
+
+- `overview.md` is loaded when a conversation starts and acts as an at-a-glance summary.
+- Topic files store deeper notes, such as team context, architecture, deployment patterns, logs, auth, debugging notes, and reusable queries.
+
+The agent updates these files by merging new information and removing outdated details. This gives the agent a durable, compact summary of what your team has learned.
+
+
+
+## Knowledge sources in the portal
+
+You can add more knowledge in [sre.azure.com](https://sre.azure.com) from **Builder** > **Knowledge sources**.
+
+Knowledge sources can include:
+
+- **Files**: Upload runbooks, architecture docs, escalation guides, API docs, and team procedures.
+- **Web pages**: Connect stable documentation pages that the agent should cite.
+- **Repositories**: Connect source repositories when code, configuration, or markdown docs should ground agent responses.
+
+Use uploaded files for stable content. Use connected sources for content that changes often, such as a wiki, repository, or live documentation site.
+
+The template upload path uses the same SRE Agent data-plane contract as the portal file upload flow: `PUT /api/v2/extendedAgent/connectors/{name}` with `type: KnowledgeItem` and `dataConnectorType: KnowledgeFile`. Validate the shipped knowledge with `bin/verify-agent.sh`, the `bin/apply-extras.sh` output, or **Builder** > **Knowledge sources**.
+
+
+
+## Knowledge across redeployments
+
+Knowledge from the template persists across redeployments through the extras configuration step.
+
+When you run `bin/deploy.sh`, the template provisions Azure resources and then runs `bin/apply-extras.sh`. The extras script uploads everything under `recipes/finops-hub/knowledge/`, uploads the shared output style, and waits for the expected KnowledgeFile sources to appear as indexed:
+
+```bash
+for file in "$REPO_ROOT"/src/templates/sre-agent/recipes/finops-hub/knowledge/*.md; do
+ PUT "$SRE_AGENT_ENDPOINT/api/v2/extendedAgent/connectors/$(basename "$file")"
+done
+PUT "$SRE_AGENT_ENDPOINT/api/v2/extendedAgent/connectors/ftk-output-style.md"
+curl -H "Authorization: Bearer $TOKEN" "$SRE_AGENT_ENDPOINT/api/v2/extendedAgent/connectors"
+```
+
+This keeps the shipped onboarding, artifact verification, Teams notification, document index, output-style, and known-issues guidance available after the agent is redeployed. If you add your own files to `recipes/finops-hub/knowledge/`, they are uploaded by the same step.
+
+
+
+## Best practices
+
+Use knowledge for information the agent should trust and cite.
+
+- Upload runbooks, architecture guides, escalation paths, known issues, connector setup notes, and service-specific troubleshooting guides.
+- Name files by task or topic, such as `aks-scale-out-runbook.md`, `finops-hub-kusto-queries.md`, or `teams-report-delivery.md`.
+- Keep one purpose per file so the agent can retrieve the right source quickly.
+- Include owner, date, scope, prerequisites, and last review notes in operational docs.
+- Avoid secrets, credentials, personal data, and raw financial figures.
+- Review knowledge quarterly to remove stale workarounds, retire old commands, and add new operational learnings.
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent)
+
+
+
+
+## Vote on or suggest ideas
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Workload management](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Microsoft Teams](/microsoftteams/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [FinOps hubs](../hubs/finops-hubs-overview.md)
+- [FinOps toolkit Power BI reports](../power-bi/reports.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/kusto-tools.md b/docs-mslearn/toolkit/sre-agent/kusto-tools.md
new file mode 100644
index 000000000..029e64f67
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/kusto-tools.md
@@ -0,0 +1,497 @@
+---
+title: Kusto tools
+description: Review the FinOps hub Kusto tools the FinOps toolkit ships for Azure SRE Agent and learn when to use each tool for cost, commitment discount, anomaly, forecast, AI, and price analysis.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: reference
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to understand each Kusto tool the FinOps toolkit ships for Azure SRE Agent so that I can choose the right query for cost and optimization analysis.
+---
+
+# Kusto tools
+
+The FinOps toolkit deployment configures Azure SRE Agent with 37 Kusto tools that query your FinOps hub Azure Data Explorer database through the `finops-hub-kusto` connector. Each tool is configured as a `KustoTool` and generated from the FinOps hub query catalog to ground agent responses in cost, price, recommendation, transaction, KPI, and AI usage data. Query source lives at [`src/queries/catalog`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/). The SRE Agent recipe rejects explicit Kusto YAML files so the query catalog stays the only source for Kusto tool definitions.
+
+This reference also calls out related optimization tools that appear in scheduled-task requirements when they affect the same analysis path. Those tools aren't Kusto tools unless explicitly marked as `KustoTool`.
+
+> [!IMPORTANT]
+> Kusto tools require the hosted Azure SRE Agent to reach the Azure Data Explorer query endpoint. If the FinOps hub cluster has `publicNetworkAccess` set to `Disabled`, the toolkit still deploys the agent, assigns `AllDatabasesViewer`, and creates the connector, but the connector is expected to remain unhealthy. Review the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations), then decide whether to enable public query access for the cluster.
+
+Use this reference when you want to understand which tool fits a prompt, scheduled task, or custom agent workflow. For a summary of all the agent's tools, see [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md).
+
+
+
+## Source validation
+
+The template generates every FinOps hub Kusto tool from the 37 `.kql` files in [`src/queries/catalog`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/). The recipe keeps 13 `PythonTool` YAML files in [`src/templates/sre-agent/recipes/finops-hub/config/tools`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/). Build validation fails if an explicit Kusto YAML file is added, if a catalog query doesn't generate exactly one Kusto tool, if a KPI-linked query isn't generated as a Kusto tool, or if a KPI-linked query isn't requested by any scheduled task.
+
+| Source category | Count | Evidence |
+|-----------------|------:|----------|
+| FinOps hub Kusto tools | 37 | Generated from [`src/queries/catalog/*.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/) by [`build-extras.py`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/bin/build-extras.py). The catalog is indexed in [`src/queries/INDEX.md`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/INDEX.md), and KPI-linked tools are mapped in [`src/queries/KPI.md`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/KPI.md). |
+| Related Python tools | 13 | [`benefit-recommendations`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml), [`capacity-reservation-groups`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml), [`data-freshness-check`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml), [`db-service-quotas`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml), [`deploy-anomaly-alert`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml), [`deploy-budget`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml), [`deploy-bulk-anomaly-alerts`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-anomaly-alerts.yaml), [`deploy-bulk-budgets`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml), [`non-compute-quotas`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml), [`resource-graph-query`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml), [`sku-availability`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml), [`suppress-advisor-recommendations`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml), and [`vm-quota-usage`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml) |
+
+
+
+## Cost analysis
+
+Use cost analysis tools to review cost and usage from different reporting angles, including time, service, resource group, region, billing hierarchy, and resource type.
+
+### costs-enriched-base
+
+Source query: [`costs-enriched-base.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/costs-enriched-base.kql).
+
+Queries a guarded, enriched row-level cost and usage sample with tags, resource details, savings fields, commitment fields, and FinOps toolkit metadata.
+
+Use it only for narrow detail drill-downs after an aggregate result identifies the scope to inspect. The tool rejects windows greater than one day to avoid Azure Data Explorer result truncation. For full-month, fiscal-period, scheduled report, dashboard export, allocation, showback, chargeback, tag-coverage, or broad custom analysis, use aggregate tools first, such as `monthly-cost-trend`, `cost-by-financial-hierarchy`, `top-services-by-cost`, `top-resource-groups-by-cost`, or `top-resource-types-by-cost`.
+
+Example prompt: "Show me enriched cost details for yesterday's top resource group, including tags and resource group."
+
+Sample output shape: One row per cost record with fields such as `ChargePeriodStart`, `SubAccountName`, `ResourceId`, `ResourceName`, `ResourceType`, `ServiceName`, `x_ResourceGroupName`, `BilledCost`, `EffectiveCost`, `ContractedCost`, `ListCost`, `PricingQuantity`, `Tags`, `CommitmentDiscountType`, `CommitmentDiscountStatus`, `x_TotalSavings`, and `x_FreeReason`.
+
+### monthly-cost-trend
+
+Source query: [`monthly-cost-trend.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/monthly-cost-trend.kql).
+
+Queries monthly billed and effective cost totals to show cost trends over time.
+
+Use it when you need a month-by-month view for budget reviews, executive summaries, or recurring FinOps reporting. It helps identify whether spend is increasing, decreasing, or stabilizing.
+
+Example prompt: "Show the monthly cost trend for the last six months."
+
+Sample output shape: One row per month with `x_ChargeMonth`, `BilledCost`, and `EffectiveCost`.
+
+### monthly-cost-change-percentage
+
+Source query: [`monthly-cost-change-percentage.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/monthly-cost-change-percentage.kql).
+
+Queries month-over-month billed and effective cost changes as percentages.
+
+Use it when stakeholders ask how much costs changed between months or when you need to find volatility quickly. It works well for variance reviews and cost spike triage.
+
+Example prompt: "Which months had the largest percentage increase in effective cost?"
+
+Sample output shape: One row per month with `ChargePeriodStart`, `BilledCost`, `EffectiveCost`, `PreviousBilledCost`, `PreviousEffectiveCost`, billed-cost change percentage, and effective-cost change percentage.
+
+### quarterly-cost-by-resource-group
+
+Source query: [`quarterly-cost-by-resource-group.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/quarterly-cost-by-resource-group.kql).
+
+Queries quarterly cost by resource group, subscription, and month for a reporting window.
+
+Use it when preparing quarterly business reviews or resource-group-level accountability reports. It helps teams connect quarterly cost movement to resource ownership and subscription context.
+
+Example prompt: "Break down quarterly costs by resource group for the last quarter."
+
+Sample output shape: One row per subscription, resource group, and month with `SubAccountName`, `x_ResourceGroupName`, `x_ChargeMonth`, and `EffectiveCost`.
+
+### cost-by-region-trend
+
+Source query: [`cost-by-region-trend.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-by-region-trend.kql).
+
+Queries effective cost trends by Azure region.
+
+Use it when you need to understand regional cost distribution, investigate regional growth, or evaluate whether workloads are shifting between regions. It can also support capacity and placement discussions.
+
+Example prompt: "Show cost trends by Azure region over the last three months."
+
+Sample output shape: One row per region with `RegionName` and `EffectiveCost`.
+
+### cost-by-financial-hierarchy
+
+Source query: [`cost-by-financial-hierarchy.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-by-financial-hierarchy.kql).
+
+Queries costs organized by billing profile, invoice section, team, product, application, environment, and other financial hierarchy fields.
+
+Use it when you need a finance-aligned view for allocation, showback, chargeback, or executive reporting. It helps translate resource-level usage into business ownership.
+
+Example prompt: "Summarize costs by billing profile, team, product, and application."
+
+Sample output shape: One row per financial hierarchy combination with `x_BillingProfileName`, `x_InvoiceSectionName`, `x_Team`, `x_Product`, `x_Application`, `x_Environment`, `EffectiveCost`, and `PercentOfTotal`.
+
+### top-services-by-cost
+
+Source query: [`top-services-by-cost.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/top-services-by-cost.kql).
+
+Queries the Azure services with the highest effective cost.
+
+Use it when you need to prioritize service-level optimization or identify which services drive overall spend. Start here for broad cost reviews before drilling into resources or resource groups.
+
+Example prompt: "What are the top 10 Azure services by cost this month?"
+
+Sample output shape: One row per service with `ServiceName` and `EffectiveCost`.
+
+### top-resource-types-by-cost
+
+Source query: [`top-resource-types-by-cost.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/top-resource-types-by-cost.kql).
+
+Queries the resource types with the highest effective cost and resource counts.
+
+Use it when you need to understand which resource categories are driving spend, such as virtual machines, disks, databases, or networking resources. It helps identify optimization themes across many resources.
+
+Example prompt: "List the top resource types by effective cost and resource count."
+
+Sample output shape: One row per resource type with `ResourceType`, `ResourceCount`, and `EffectiveCost`.
+
+### top-resource-groups-by-cost
+
+Source query: [`top-resource-groups-by-cost.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/top-resource-groups-by-cost.kql).
+
+Queries the resource groups with the highest effective cost.
+
+Use it when you need an owner-friendly cost view or want to focus optimization work on the most expensive resource groups. It is useful for team accountability and workload-level reviews.
+
+Example prompt: "Which resource groups had the highest costs in the last 30 days?"
+
+Sample output shape: One row per subscription and resource group with `SubAccountName`, `x_ResourceGroupName`, and `EffectiveCost`.
+
+
+
+## Commitment discounts
+
+Use commitment discount tools to review reservation and savings plan utilization, recommendations, realized savings, and purchase transactions.
+
+### commitment-discount-utilization
+
+Source query: [`commitment-discount-utilization.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/commitment-discount-utilization.kql).
+
+Queries consumed core hours by commitment discount type, including reservation, savings plan, and on-demand usage.
+
+Use it when you need to understand how well commitments are being used and whether uncovered on-demand usage remains. It helps compare capacity planning, usage patterns, and commitment coverage.
+
+Example prompt: "Show reservation and savings plan utilization for the last month."
+
+Sample output shape: One row per commitment category with `CommitmentDiscountType`, `TotalConsumedCoreHours`, and `PercentOfTotal`. Empty commitment types are reported as `On Demand`.
+
+### reservation-recommendation-breakdown
+
+Source query: [`reservation-recommendation-breakdown.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/reservation-recommendation-breakdown.kql).
+
+Queries detailed reservation recommendations, including savings, break-even dates, normalized sizes, scope, and term details.
+
+Use it when evaluating whether to buy reservations or when preparing a recommendation package for finance and workload owners. It helps compare potential savings with commitment risk.
+
+Example prompt: "Break down reservation purchase recommendations by service, term, and expected savings."
+
+Sample output shape: One row per reservation recommendation with fields such as `RegionId`, normalized size or group details, `x_SkuMeterId`, `x_SkuTerm`, expected before and after effective cost, `x_BreakEvenMonths`, and `x_BreakEvenDate`.
+
+### benefit-recommendations
+
+Source query: [`benefit-recommendations.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/benefit-recommendations.kql).
+
+Gets Microsoft Cost Management benefit recommendations for savings plans and reserved instances at a billing scope.
+
+This is a related optimization tool, but it's a `PythonTool`, not a Kusto tool. Use it when the agent needs current Cost Management recommendation API results by billing scope, lookback period, and term. Use `reservation-recommendation-breakdown` when you need FinOps hub recommendation data from `Recommendations()`.
+
+Example prompt: "Get current savings plan and reservation benefit recommendations for this billing profile."
+
+Sample output shape: A JSON object with `billing_scope`, `lookback_period`, `term`, `count`, and `recommendations`. Each recommendation includes fields such as `type`, `savings`, `cost`, `total_cost`, `cost_without_benefit`, `term`, `break_even`, `id`, and `name`. If the request can't run, the object includes an `error` field.
+
+### savings-summary-report
+
+Source query: [`savings-summary-report.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/savings-summary-report.kql).
+
+Queries list cost, effective cost, negotiated savings, commitment savings, total savings, and savings rate.
+
+Use it when you need a summary of savings from discounts and commitments compared with pay-as-you-go pricing. It works well for executive reporting and rate optimization reviews.
+
+Example prompt: "Summarize total savings from negotiated rates and commitment discounts this month."
+
+Sample output shape: One row per billing currency with `BillingCurrency`, `ListCost`, `ContractedCost`, `EffectiveCost`, `x_NegotiatedDiscountSavings`, `x_CommitmentDiscountSavings`, `x_TotalSavings`, and `x_EffectiveSavingsRate`.
+
+### top-commitment-transactions
+
+Source query: [`top-commitment-transactions.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/top-commitment-transactions.kql).
+
+Queries the largest non-usage commitment discount purchase transactions, including reservations and savings plans.
+
+Use it when investigating major commitment-related charges, purchase timing, or renewal activity. It helps separate commitment purchases from normal usage costs.
+
+Example prompt: "Show the largest reservation and savings plan transactions this quarter."
+
+Sample output shape: One row per commitment purchase transaction with `ChargePeriodStart`, `ChargeCategory`, `BilledCost`, `BillingCurrency`, `CommitmentDiscountName`, `CommitmentDiscountNameUnique`, `CommitmentDiscountType`, `SubAccountName`, `SubAccountNameUnique`, `ResourceNameUnique`, and `x_ResourceGroupNameUnique`.
+
+
+
+## Anomaly detection
+
+Use anomaly detection tools to identify unusual cost patterns that need investigation.
+
+### cost-anomaly-detection
+
+Source query: [`cost-anomaly-detection.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-anomaly-detection.kql).
+
+Queries cost time series and detects unusual spikes or drops across a configurable history window.
+
+Use it when you need to triage unexpected spend changes, monitor recurring cost health, or explain why costs moved outside the normal pattern. Pair it with cost drill-down tools to find the affected service, region, or resource group.
+
+Example prompt: "Detect cost anomalies over the last 90 days and explain the biggest spikes."
+
+Sample output shape: One time-series row with `ChargePeriodStart`, `CostSeries`, and `anomalies`, where the series arrays represent the analyzed interval and the anomaly markers.
+
+
+
+## Forecasting
+
+Use forecasting tools to project future cost based on historical patterns.
+
+### cost-forecasting-model
+
+Source query: [`cost-forecasting-model.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-forecasting-model.kql).
+
+Queries historical cost trends and projects future effective cost.
+
+Use it when preparing budgets, rolling forecasts, or expected run-rate discussions. It is most helpful when recent historical usage is representative of near-term demand.
+
+Example prompt: "Forecast effective cost for the next three months based on recent trends."
+
+Sample output shape: One time-series row with `ChargePeriodStart`, `EffectiveCostSeries`, and `forecast`, where the forecast array extends the historical cost series for the requested future periods.
+
+
+
+## AI/ML costs
+
+Use AI/ML cost tools to analyze Azure OpenAI and related AI service costs, token usage, model efficiency, and application ownership.
+
+### ai-cost-by-application
+
+Source query: [`ai-cost-by-application.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/ai-cost-by-application.kql).
+
+Queries AI and machine learning costs by application, team, and environment tags.
+
+Use it when you need showback, chargeback, or ownership analysis for AI workloads. It helps map AI spend to the applications and teams consuming it.
+
+Example prompt: "Break down AI costs by application and environment for this month."
+
+Sample output shape: One row per application, team, environment, and cost center with fields such as `Application`, `Team`, `Environment`, `CostCenter`, `EffectiveCost`, `TokenCount`, and `CostPer1KTokens`.
+
+### ai-daily-trend
+
+Source query: [`ai-daily-trend.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/ai-daily-trend.kql).
+
+Queries daily AI and machine learning cost trends.
+
+Use it when you need to monitor day-to-day AI spend, spot sudden changes, or prepare daily operating reports. It can also provide context before investigating token or model-level drivers.
+
+Example prompt: "Show the daily AI cost trend for the last 30 days."
+
+Sample output shape: One row per day with `ChargePeriodStart`, daily AI cost, daily token count, and cost per 1,000 tokens.
+
+### ai-model-cost-comparison
+
+Source query: [`ai-model-cost-comparison.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/ai-model-cost-comparison.kql).
+
+Queries AI model costs so the agent can compare costs across models.
+
+Use it when evaluating model efficiency, unit economics, or opportunities to shift workloads to lower-cost models. It is useful for comparing model choices before changing application behavior.
+
+Example prompt: "Compare costs across AI models and identify the most expensive models per 1,000 tokens."
+
+Sample output shape: One row per model with `Model`, `TokenCount`, `EffectiveCost`, `ListCost`, `CostPer1KTokens`, `ListPer1KTokens`, and `DiscountPercent`.
+
+### ai-token-usage-breakdown
+
+Source query: [`ai-token-usage-breakdown.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/ai-token-usage-breakdown.kql).
+
+Queries AI token usage by model, model version, and input or output direction.
+
+Use it when token consumption is the suspected cost driver or when you need to separate prompt and completion usage. It helps connect AI cost changes to usage behavior.
+
+Example prompt: "Break down token usage by model and input versus output tokens for the last week."
+
+Sample output shape: One row per model and token direction with `Model`, `Direction`, `TokenCount`, `EffectiveCost`, `UnitCostPerToken`, and `CostPer1KTokens`.
+
+
+
+## FinOps KPI tools
+
+FinOps KPI tools are generated from the query catalog and map to query links in [`src/queries/KPI.md`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/KPI.md). Scheduled tasks request these tools through `ftk-database-query` for monthly, semiannual, health, anomaly, capacity, storage, monitoring, AI, and benefit-review scorecards.
+
+### percentage-unallocated-costs
+
+Source query: [`percentage-unallocated-costs.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/percentage-unallocated-costs.kql).
+
+Measures the share of effective cost that lacks required allocation evidence for the reporting window.
+
+### percentage-untagged-costs
+
+Source query: [`percentage-untagged-costs.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/percentage-untagged-costs.kql).
+
+Measures the share of effective cost on resources that have no tags.
+
+### tagging-policy-compliance
+
+Source query: [`tagging-policy-compliance.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/tagging-policy-compliance.kql).
+
+Measures cost-weighted compliance with required tag keys.
+
+### allocation-accuracy-index
+
+Source query: [`allocation-accuracy-index.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/allocation-accuracy-index.kql).
+
+Measures directly attributed cost as a share of total effective cost.
+
+### anomaly-detection-rate
+
+Source query: [`anomaly-detection-rate.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/anomaly-detection-rate.kql).
+
+Measures the share of effective spend in anomaly-flagged daily buckets.
+
+### anomaly-variance-total
+
+Source query: [`anomaly-variance-total.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/anomaly-variance-total.kql).
+
+Quantifies signed and absolute unpredicted spend variance for detected anomaly events.
+
+### cost-visibility-delay
+
+Source query: [`cost-visibility-delay.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-visibility-delay.kql).
+
+Measures cost data visibility delay from charge period end to Hub ingestion.
+
+### data-update-frequency
+
+Source query: [`data-update-frequency.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/data-update-frequency.kql).
+
+Measures FinOps hub ingestion update cadence from distinct ingestion timestamps.
+
+### commitment-utilization-score
+
+Source query: [`commitment-utilization-score.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/commitment-utilization-score.kql).
+
+Computes commitment utilization amount, potential, and score by commitment and currency.
+
+### commitment-discount-waste
+
+Source query: [`commitment-discount-waste.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/commitment-discount-waste.kql).
+
+Measures unused commitment value as a share of total commitment cost.
+
+### compute-spend-commitment-coverage
+
+Source query: [`compute-spend-commitment-coverage.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/compute-spend-commitment-coverage.kql).
+
+Measures compute spend covered by commitment discounts.
+
+### compute-cost-per-core
+
+Source query: [`compute-cost-per-core.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/compute-cost-per-core.kql).
+
+Computes hourly and effective average compute cost per consumed vCPU core hour.
+
+### cost-optimization-index
+
+Source query: [`cost-optimization-index.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-optimization-index.kql).
+
+Computes the Hub-wide cost optimization index from current recommendations and cost context.
+
+### cost-per-gb-stored
+
+Source query: [`cost-per-gb-stored.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/cost-per-gb-stored.kql).
+
+Calculates storage cost per normalized GB-month.
+
+### macc-consumption-vs-commitment
+
+Source query: [`macc-consumption-vs-commitment.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/macc-consumption-vs-commitment.kql).
+
+Measures Microsoft Azure Consumption Commitment drawdown by billing profile and month.
+
+### storage-tier-distribution
+
+Source query: [`storage-tier-distribution.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/storage-tier-distribution.kql).
+
+Summarizes storage cost and GB-month distribution by access-tier bucket.
+
+
+
+## Usage optimization
+
+Use Usage Optimization tools to identify idle, orphaned, or wasteful resources before starting cleanup work.
+
+### idle-resource-sweep
+
+Source status: No `idle-resource-sweep.yaml` file appears in the source inventory for [`src/templates/sre-agent/recipes/finops-hub/config/tools`](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent/recipes/finops-hub/config/tools/).
+
+Reviews idle or orphaned resource candidates for Usage Optimization.
+
+This Gate-named tool is a required analysis path, but the current template doesn't include an `idle-resource-sweep` Kusto tool YAML file. Until a dedicated tool is added, ground idle-resource reviews with `top-resource-types-by-cost`, `top-resource-groups-by-cost`, and narrow `costs-enriched-base` drill-downs. Then correlate candidates with Azure Resource Graph or Azure Advisor data through the relevant Python or built-in Azure tools before recommending cleanup.
+
+Example prompt: "Find likely idle or orphaned resources and group them by resource type, subscription, and estimated monthly cost."
+
+Sample output shape: A candidate list with fields such as `SubAccountName`, `ResourceId`, `ResourceName`, `ResourceType`, `x_ResourceGroupName`, `RegionName`, `ServiceName`, `EffectiveCost`, `LastUsageSignal`, `IdleReason`, `RecommendedAction`, and `Confidence`. When using the current template, this shape is assembled from existing cost aggregates, enriched cost rows, and external Azure inventory or recommendation signals rather than returned by a single shipped Kusto tool.
+
+
+
+## Price analysis
+
+Use price analysis tools to compare prices, savings, and non-commitment transactions that affect total cost.
+
+### service-price-benchmarking
+
+Source query: [`service-price-benchmarking.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/service-price-benchmarking.kql).
+
+Queries service price benchmarks, including list cost, contracted cost, effective cost, negotiated savings, commitment savings, and total savings.
+
+Use it when you need to compare prices across services or understand how negotiated and commitment discounts affect effective rates. It helps identify services with the largest rate optimization opportunity.
+
+Example prompt: "Benchmark service prices and show where negotiated or commitment savings are highest."
+
+Sample output shape: One row per service with `ServiceName`, `ListCost`, `ContractedCost`, `EffectiveCost`, negotiated savings, commitment discount savings, total savings, and savings percentages.
+
+### top-other-transactions
+
+Source query: [`top-other-transactions.kql`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/top-other-transactions.kql).
+
+Queries the largest non-usage and non-commitment transactions, such as Marketplace or miscellaneous charges.
+
+Use it when costs don't reconcile to normal usage or commitment purchases. It helps isolate large one-time or nonstandard transactions that can distort monthly cost trends.
+
+Example prompt: "Show the largest non-usage and non-commitment transactions this month."
+
+Sample output shape: One row per non-usage, non-commitment transaction with `ChargePeriodStart`, `ChargeCategory`, `BilledCost`, `BillingCurrency`, `SubAccountName`, `x_InvoiceSectionName`, `PricingCategory`, `PricingQuantity`, `PricingUnit`, `ProviderName`, and `PublisherName`.
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/KustoTools)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Cost allocation](../../framework/understand/allocation.md)
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Microsoft Cost Management](/azure/cost-management-billing/costs/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md)
+- [Python tools](python-tools.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/overview.md b/docs-mslearn/toolkit/sre-agent/overview.md
new file mode 100644
index 000000000..3c8433299
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/overview.md
@@ -0,0 +1,151 @@
+---
+title: Azure SRE Agent in the FinOps toolkit
+description: Learn how the FinOps toolkit deploys Azure SRE Agent with FinOps and capacity management automation on top of FinOps hubs.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: concept-article
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to understand what the FinOps toolkit's Azure SRE Agent deployment provides so that I can automate cost, capacity, and operations workflows.
+---
+
+# Azure SRE Agent in the FinOps toolkit
+
+The FinOps toolkit ships a Deploy to Azure template and a local Azure CLI + Bicep wrapper that deploy [Azure SRE Agent](/azure/sre-agent/overview) and configure it for FinOps and capacity management workflows on top of [FinOps hubs](../hubs/finops-hubs-overview.md). The deployment includes specialist subagents, FOCUS-aligned Kusto and Python tools, scheduled tasks, and grounded knowledge so the agent can investigate cost changes, monitor quota and capacity signals, prepare executive summaries, and deliver scheduled updates. The deployment focuses on three core design principles:
+
+- **Automate the rhythm**
_Run daily, weekly, monthly, and quarterly FinOps workflows without waiting for manual report requests._
+- **Ground every answer**
_Use FinOps hub data, FOCUS-aligned Kusto tools, and Azure platform context to keep recommendations tied to evidence._
+- **Act with experts**
_Route work to specialized FinOps, finance, capacity, database, and hubs agents instead of a single generic assistant._
+
+The FinOps toolkit deployment helps teams move from dashboards and alerts to an operating model where the agent investigates cost changes, monitors quota and capacity signals, prepares executive summaries, and delivers scheduled updates through Azure SRE Agent.
+
+
+> [!NOTE]
+> Estimated cost: Varies by Azure SRE Agent preview pricing, telemetry ingestion, and your existing FinOps hub footprint.
+>
+> The template provisions an Azure SRE Agent with a system-assigned managed identity, Log Analytics workspace, and Application Insights resource. Costs depend on the selected region, Azure SRE Agent pricing, Log Analytics and Application Insights ingestion and retention, and the Azure Data Explorer footprint you connect to. The managed identity and RBAC assignments don't typically add direct cost.
+
+
+
+> [!IMPORTANT]
+> The FinOps toolkit deploys all SRE Agent resources even when the connected Azure Data Explorer cluster uses private endpoints. If the cluster has `publicNetworkAccess` set to `Disabled`, hosted Azure SRE Agent can't run direct KQL queries against the cluster. The deployment warns and links to the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations) so the customer can decide whether to enable public query access.
+
+
+
+
+
+> [!div class="nextstepaction"]
+> [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+
+
+
+
+## What you get
+
+The FinOps toolkit's Azure SRE Agent template deploys and configures these resources and Azure SRE Agent objects:
+
+| Component | Count | Description |
+|-----------|-------|-------------|
+| Azure SRE Agent | 1 | `Microsoft.App/agents` resource in autonomous mode |
+| Model provider | 1 | Azure OpenAI provider-level routing (`MicrosoftFoundry` ARM value, `Automatic` model routing) |
+| Managed identities | 1-2 | System-assigned managed identity for the agent; portal deployments also create a user-assigned identity for the deployment script |
+| Log Analytics | 1 | Workspace for agent telemetry |
+| Application Insights | 1 | Linked to Log Analytics for monitoring |
+| Target resource group RBAC | 3-4 per target group | Reader, Monitoring Reader, Log Analytics Reader, and Contributor when `accessLevel` is `High` |
+| Azure Data Explorer role | Optional | `AllDatabasesViewer` when Azure Data Explorer parameters are provided |
+| Custom agents | 5 | `azure-capacity-manager`, `chief-financial-officer`, `finops-practitioner`, `ftk-database-query`, and `ftk-hubs-agent` |
+| Skills | 3 | `azure-capacity-management`, `azure-cost-management`, and `finops-toolkit` |
+| Tools | 50 | Kusto, capacity, and Hub infrastructure tools |
+| Tool overrides | 9 | Enables SRE Agent Log Query and Visualization tools |
+| Scheduled tasks | 19 | Daily, weekly, monthly, semiannual, and quarterly recurring workflows |
+| Connectors | 1 | Optional Kusto connector to your FinOps hub Azure Data Explorer cluster |
+| Knowledge docs | 6 | Five recipe knowledge docs plus the FinOps Toolkit output style |
+| Notification connectors | 0 by default | Outlook and Teams can be added after deployment in the Azure SRE Agent portal |
+
+
+
+## Architecture overview
+
+The FinOps toolkit deployment is copied from the Microsoft SRE Agent starter-lab pattern and updated to use the toolkit's packaged Deploy to Azure flow or Azure CLI + Bicep directly. It doesn't use `azd`. The deployment runs in this order:
+
+1. The Azure portal or `bin/deploy.sh` starts a subscription-scoped ARM deployment.
+2. The deployment creates the agent resource group when needed.
+3. Bicep creates monitoring resources and Azure SRE Agent.
+4. Bicep assigns target resource group RBAC and can optionally assign `AllDatabasesViewer` on your FinOps hub Azure Data Explorer cluster.
+5. A deployment script in the portal path, or `bin/apply-extras.sh` in the local CLI path, creates the Kusto connector and applies skills, custom agents, tools, knowledge, and scheduled tasks through the supported SRE Agent ARM and data-plane surfaces.
+6. You optionally add Outlook and Teams connectors in [sre.azure.com](https://sre.azure.com) when you want scheduled reports delivered outside the agent chat.
+
+The result is a single Azure SRE Agent with a FinOps Framework-aligned operating model layered on top. The `finops-practitioner` agent owns the FinOps operating rhythm and scheduled analysis, has no direct tools, and delegates evidence collection to specialists. The `ftk-database-query` agent owns all Kusto and FOCUS evidence collection. The `azure-capacity-manager` agent maps Azure capacity signals into Planning & Estimating, Forecasting, Architecting & Workload Placement, Usage Optimization, Rate Optimization support, Governance, Policy & Risk, and Automation, Tools & Services. The `chief-financial-officer` agent provides finance and leadership consultation, and the `ftk-hubs-agent` monitors FinOps hub health and data freshness.
+
+The template defaults the parent SRE Agent to Azure OpenAI provider-level routing (`MicrosoftFoundry` in ARM) with automatic model selection inside that provider. Individual custom agents don't pin model names; they inherit the parent agent provider and use SRE Agent's automatic model routing.
+
+
+
+## When to use the agent
+
+FinOps teams often have the data they need but not the time to run every investigation on schedule. The agent gives the team an automated operating rhythm that can review cost changes, check commitment coverage, monitor capacity headroom, and summarize action items before the next stakeholder meeting.
+
+Use the agent when you want to:
+
+- Review FinOps hub data through conversational and scheduled agent workflows.
+- Monitor cost anomalies, budget variance, and forecast drift.
+- Track quota usage, capacity reservation utilization, region access, and zone readiness.
+- Prepare daily, weekly, monthly, and quarterly cost and capacity summaries.
+- Route questions to specialized agents with FinOps, finance, capacity, database, and hubs context.
+
+
+
+## Get started
+
+Deploy the agent when you have a FinOps hub with an Azure Data Explorer cluster and an Azure subscription where you can deploy Azure SRE Agent resources. After deployment, open the agent in [sre.azure.com](https://sre.azure.com), verify the subagents, skills, tools, and scheduled tasks, and add notification connectors if you want reports sent to Teams or Outlook.
+
+
+> [!div class="nextstepaction"]
+> [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Architecting for cloud](../../framework/optimize/architecting.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+- [Usage optimization](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Azure Monitor](/azure/azure-monitor/)
+
+Related solutions:
+
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [FinOps hubs](../hubs/finops-hubs-overview.md)
+- [Azure SRE Agent template reference (FinOps toolkit)](template.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/python-tools.md b/docs-mslearn/toolkit/sre-agent/python-tools.md
new file mode 100644
index 000000000..01a869551
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/python-tools.md
@@ -0,0 +1,514 @@
+---
+title: Python tools
+description: Review the Python tools the FinOps toolkit ships for Azure SRE Agent for Azure quota, capacity, budgets, anomaly alerts, Resource Graph, FinOps hub health, and Advisor suppressions.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: reference
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to understand which Python tools the FinOps toolkit ships for Azure SRE Agent so that I can use the right Azure API-backed tool for capacity, cost governance, and operations work.
+---
+
+# Python tools
+
+The FinOps toolkit deployment configures Azure SRE Agent with 13 Python tools that call Azure APIs through the agent's managed identity. These tools complement the Kusto tools by checking Azure platform state, deploying Cost Management controls, and automating governance tasks that aren't stored in FinOps hub data.
+
+Use Python tools when the agent needs live Azure Resource Manager, Resource Graph, Cost Management, Azure Data Explorer, or Advisor data instead of historical cost and usage data from the FinOps hub.
+
+> [!NOTE]
+> The agent managed identity must have enough permission at the target scope to read or update the resources each tool touches. Read tools typically need Reader permissions. Deployment and suppression tools need permissions to create or update the target Cost Management or Advisor resources.
+
+## Source inventory
+
+The following table maps the 13 Python tools documented on this page to their source YAML files. Use this inventory to audit this reference page against the template source. It doesn't assert that no other YAML files exist under `src/templates/sre-agent/recipes/finops-hub/config/tools/`.
+
+| Tool | Source file |
+|------|-------------|
+| `vm-quota-usage` | `src/templates/sre-agent/recipes/finops-hub/config/tools/vm-quota-usage.yaml` |
+| `capacity-reservation-groups` | `src/templates/sre-agent/recipes/finops-hub/config/tools/capacity-reservation-groups.yaml` |
+| `sku-availability` | `src/templates/sre-agent/recipes/finops-hub/config/tools/sku-availability.yaml` |
+| `non-compute-quotas` | `src/templates/sre-agent/recipes/finops-hub/config/tools/non-compute-quotas.yaml` |
+| `deploy-budget` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-budget.yaml` |
+| `deploy-bulk-budgets` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-budgets.yaml` |
+| `deploy-anomaly-alert` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-anomaly-alert.yaml` |
+| `deploy-bulk-anomaly-alerts` | `src/templates/sre-agent/recipes/finops-hub/config/tools/deploy-bulk-anomaly-alerts.yaml` |
+| `resource-graph-query` | `src/templates/sre-agent/recipes/finops-hub/config/tools/resource-graph-query.yaml` |
+| `benefit-recommendations` | `src/templates/sre-agent/recipes/finops-hub/config/tools/benefit-recommendations.yaml` |
+| `data-freshness-check` | `src/templates/sre-agent/recipes/finops-hub/config/tools/data-freshness-check.yaml` |
+| `db-service-quotas` | `src/templates/sre-agent/recipes/finops-hub/config/tools/db-service-quotas.yaml` |
+| `suppress-advisor-recommendations` | `src/templates/sre-agent/recipes/finops-hub/config/tools/suppress-advisor-recommendations.yaml` |
+
+
+
+## Capacity and quota
+
+Use capacity and quota tools to check VM family quota, capacity reservation group utilization, SKU restrictions, and non-compute service limits before deployment or during recurring capacity reviews.
+
+### `vm-quota-usage`
+
+- **Azure API:** Azure Resource Manager Compute usages API: `Microsoft.Compute/locations/usages`.
+- **When to use it:** Use this tool to query VM family quota usage across one or more regions in a subscription. It calculates utilization percentages and flags VM families above warning or critical thresholds.
+- **Example prompt:** "Check VM quota usage for subscription `` in eastus and westus2, and call out any families above 80% utilization."
+- **Sample output shape:**
+
+ ```json
+ {
+ "subscription_id": "",
+ "locations": ["eastus", "westus2"],
+ "quotas": [
+ {
+ "location": "eastus",
+ "name": "Standard DSv5 Family vCPUs",
+ "current": 80,
+ "limit": 100,
+ "utilization_pct": 80,
+ "at_risk_80": false,
+ "at_risk_95": false
+ }
+ ],
+ "warning_count": 0,
+ "critical_count": 0,
+ "suppressed_error_count": 0,
+ "suppressed_errors": [],
+ "errors": []
+ }
+ ```
+
+### `capacity-reservation-groups`
+
+- **Azure API:** Azure Resource Manager Compute capacity reservation group API: `Microsoft.Compute/capacityReservationGroups`, including capacity reservation instance view and associated virtual machines.
+- **When to use it:** Use this tool to list capacity reservation groups in a subscription and compare reserved capacity with allocated virtual machines. It helps find unused capacity, overallocated groups, and zone-specific capacity reservation waste.
+- **Example prompt:** "List capacity reservation groups in subscription `` and identify groups with unused reserved capacity."
+- **Sample output shape:**
+
+ ```json
+ {
+ "subscription_id": "",
+ "capacity_reservation_groups": [
+ {
+ "name": "crg-prod-eastus",
+ "id": "/subscriptions//resourceGroups//providers/Microsoft.Compute/capacityReservationGroups/crg-prod-eastus",
+ "resource_group": "",
+ "location": "eastus",
+ "zones": ["1"],
+ "zone": "1",
+ "reserved_count": 10,
+ "allocated_count": 8,
+ "utilization_pct": 80,
+ "waste": true,
+ "waste_count": 2,
+ "overallocated": false,
+ "capacity_reservations": ["crg-prod-eastus/Standard_D8s_v5"],
+ "virtual_machines_allocated": ["/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/vm01"],
+ "virtual_machines_associated": []
+ }
+ ],
+ "errors": []
+ }
+ ```
+
+### `sku-availability`
+
+- **Azure API:** Azure Resource Manager SKU APIs for Azure Compute and Azure Data Explorer: `Microsoft.Compute/skus` and `Microsoft.Kusto/locations/skus`.
+- **When to use it:** Use this tool to verify whether a VM or Azure Data Explorer SKU is available in a region and whether zone or subscription restrictions could block deployment.
+- **Example prompt:** "Check whether Standard_D8s_v5 is available in eastus2, including zone support and any restriction reasons."
+- **Sample output shape:**
+
+ ```json
+ {
+ "subscription_id": "",
+ "location": "eastus2",
+ "sku_filter": "Standard_D8s_v5",
+ "resource_provider": "compute",
+ "service": "Azure Compute",
+ "source_endpoint": "https://management.azure.com/subscriptions//providers/Microsoft.Compute/skus?api-version=2021-07-01&$filter=location eq 'eastus2'",
+ "skus": [
+ {
+ "name": "Standard_D8s_v5",
+ "family": "standardDSv5Family",
+ "size": "D8s_v5",
+ "zones_available": ["1", "2", "3"],
+ "restrictions": []
+ }
+ ],
+ "restriction_summary": {
+ "total_skus": 1,
+ "restricted_skus": 0,
+ "restriction_count": 0,
+ "reason_codes": {}
+ }
+ }
+ ```
+
+### `non-compute-quotas`
+
+- **Azure API:** Azure Resource Manager provider usage APIs for supported services, with Azure Resource Graph fallbacks for services that don't expose a direct usage API.
+- **When to use it:** Use this tool to review storage, networking, and PaaS quota usage that can block growth even when compute quota is healthy. It marks whether each limit came from an API-reported quota or an estimated fallback.
+- **Example prompt:** "Audit non-compute quota usage for subscription `` in eastus and summarize storage or network limits above 80% utilization."
+- **Sample output shape:**
+
+ ```json
+ {
+ "subscription_id": "",
+ "services": [
+ {
+ "service_name": "Storage",
+ "resource_type": "Microsoft.Storage/locations/usages",
+ "quota_name": "StorageAccounts",
+ "current_count": 100,
+ "limit": 250,
+ "utilization_pct": 40,
+ "at_risk": false,
+ "count_source": "arm_provider_usages_api",
+ "limit_source": "api_reported_limit",
+ "limit_type": "api_reported",
+ "source_endpoint": "https://management.azure.com/subscriptions//providers/Microsoft.Storage/locations/eastus/usages?api-version=2023-05-01",
+ "location": "eastus",
+ "scope": "regional"
+ }
+ ],
+ "quotas": [
+ {
+ "subscription_id": "",
+ "service_name": "Storage",
+ "service": "Storage",
+ "resource_type": "Microsoft.Storage/locations/usages",
+ "quota_name": "StorageAccounts",
+ "name": "StorageAccounts",
+ "location": "eastus",
+ "scope": "regional",
+ "current_count": 100,
+ "current": 100,
+ "limit": 250,
+ "utilization_pct": 40,
+ "at_risk": false,
+ "at_risk_80": false,
+ "at_risk_95": false,
+ "count_source": "arm_provider_usages_api",
+ "limit_source": "api_reported_limit",
+ "limit_type": "api_reported",
+ "source_endpoint": "https://management.azure.com/subscriptions//providers/Microsoft.Storage/locations/eastus/usages?api-version=2023-05-01"
+ }
+ ],
+ "at_risk_count": 0,
+ "api_reported_limit_count": 1,
+ "estimated_limit_count": 0,
+ "suppressed_count": 0,
+ "errors": []
+ }
+ ```
+
+
+
+## Budget and alert deployment
+
+Use budget and alert deployment tools to create Cost Management controls for a single subscription or apply the same control across enabled subscriptions in a management group.
+
+### `deploy-budget`
+
+- **Azure API:** Azure Resource Manager Consumption budgets API: `Microsoft.Consumption/budgets`.
+- **When to use it:** Use this tool to create or update a subscription-level Cost Management budget with notification thresholds and contact emails.
+- **Example prompt:** "Create a monthly budget named SubscriptionBudget for subscription `` with an amount of 50000 and notify finops@example.com."
+- **Sample output shape:**
+
+ ```json
+ {
+ "subscription_id": "",
+ "budget_name": "SubscriptionBudget",
+ "amount": 50000,
+ "time_grain": "Monthly",
+ "contact_emails": ["finops@example.com"],
+ "status_code": 201,
+ "budget": {
+ "id": "/subscriptions//providers/Microsoft.Consumption/budgets/SubscriptionBudget",
+ "name": "SubscriptionBudget",
+ "type": "Microsoft.Consumption/budgets"
+ },
+ "status": "created"
+ }
+ ```
+
+### `deploy-bulk-budgets`
+
+- **Azure API:** Azure Resource Graph to discover enabled subscriptions, then Azure Resource Manager Consumption budgets API: `Microsoft.Consumption/budgets`.
+- **When to use it:** Use this tool to deploy the same Cost Management budget across all enabled subscriptions under a management group.
+- **Example prompt:** "Deploy a monthly 50000 budget named SubscriptionBudget to every enabled subscription in management group `` and notify finops@example.com."
+- **Sample output shape:**
+
+ ```json
+ {
+ "management_group": "",
+ "budget_name": "SubscriptionBudget",
+ "amount": 50000,
+ "contact_emails": ["finops@example.com"],
+ "subscription_count": 2,
+ "created_or_updated": 2,
+ "failed": 0,
+ "deployments": [
+ {
+ "subscription_id": "",
+ "subscription_name": "Production",
+ "budget_name": "SubscriptionBudget",
+ "status": "created",
+ "status_code": 201,
+ "error": null
+ }
+ ]
+ }
+ ```
+
+### `deploy-anomaly-alert`
+
+- **Azure API:** Azure Resource Manager Cost Management scheduled actions API: `Microsoft.CostManagement/scheduledActions`.
+- **When to use it:** Use this tool to create or update a daily Cost Management anomaly alert scheduled action for one subscription.
+- **Example prompt:** "Create a cost anomaly alert for subscription `` that sends daily anomaly notifications to finops@example.com."
+- **Sample output shape:**
+
+ ```json
+ {
+ "subscription_id": "",
+ "scheduled_action_name": "cost-anomaly-alert",
+ "email_recipients": ["finops@example.com"],
+ "status_code": 201,
+ "anomaly_alert": {
+ "id": "/subscriptions//providers/Microsoft.CostManagement/scheduledActions/cost-anomaly-alert",
+ "name": "cost-anomaly-alert",
+ "kind": "InsightAlert"
+ },
+ "status": "created"
+ }
+ ```
+
+### `deploy-bulk-anomaly-alerts`
+
+- **Azure API:** Azure Resource Graph to discover enabled subscriptions, then Azure Resource Manager Cost Management scheduled actions API: `Microsoft.CostManagement/scheduledActions`.
+- **When to use it:** Use this tool to deploy the same cost anomaly alert configuration across enabled subscriptions under a management group.
+- **Example prompt:** "Deploy cost anomaly alerts to all enabled subscriptions in management group `` and send notifications to finops@example.com."
+- **Sample output shape:**
+
+ ```json
+ {
+ "management_group": "",
+ "email_recipients": ["finops@example.com"],
+ "subscription_count": 2,
+ "created_or_updated": 2,
+ "failed": 0,
+ "deployments": [
+ {
+ "subscription_id": "",
+ "subscription_name": "Production",
+ "scheduled_action_name": "cost-anomaly-alert",
+ "status": "updated",
+ "status_code": 200,
+ "error": null
+ }
+ ]
+ }
+ ```
+
+
+
+## Resource analysis
+
+Use resource analysis tools when the agent needs live Azure inventory, configuration, or commitment discount recommendation data outside the FinOps hub.
+
+### `resource-graph-query`
+
+- **Azure API:** Azure Resource Graph resources API: `Microsoft.ResourceGraph/resources`.
+- **When to use it:** Use this tool to run Azure Resource Graph KQL across one or more subscriptions for inventory, configuration drift, governance checks, and subscription-scale troubleshooting.
+- **Example prompt:** "Run a Resource Graph query across my subscriptions to list unattached managed disks by subscription, resource group, size, and age."
+- **Sample output shape:**
+
+ ```json
+ {
+ "rows": [
+ {
+ "subscriptionId": "",
+ "resourceGroup": "",
+ "name": "disk01",
+ "type": "microsoft.compute/disks"
+ }
+ ],
+ "count": 1,
+ "errors": [],
+ "total_records": 1,
+ "result_truncated": false,
+ "subscriptions": [""]
+ }
+ ```
+
+### `benefit-recommendations`
+
+- **Azure API:** Azure Resource Manager Cost Management benefit recommendations API: `Microsoft.CostManagement/benefitRecommendations`.
+- **When to use it:** Use this tool to retrieve reservation and savings plan recommendations at a billing scope, including recommendation type, term, savings, cost, and break-even details.
+- **Example prompt:** "Get three-year benefit recommendations for billing account scope `` using a Last30Days lookback and summarize the largest savings opportunities."
+- **Sample output shape:**
+
+ ```json
+ {
+ "billing_scope": "",
+ "lookback_period": "Last30Days",
+ "term": "P3Y",
+ "recommendations": [
+ {
+ "type": "SP",
+ "savings": 12000,
+ "cost": 30000,
+ "total_cost": 30000,
+ "cost_without_benefit": 42000,
+ "term": "P3Y",
+ "break_even": "P8M",
+ "id": "/providers/Microsoft.Billing/billingAccounts//providers/Microsoft.CostManagement/benefitRecommendations/",
+ "name": ""
+ }
+ ],
+ "count": 1
+ }
+ ```
+
+
+
+## Hub management
+
+Use hub management tools to verify whether FinOps hub data is current enough for reliable scheduled reports and agent answers. Use `data-freshness-check` as the source of truth for hub freshness before relying on stale memory, raw KQL rollups, or ingestion timestamp checks.
+
+### `data-freshness-check`
+
+- **Azure API:** Azure Data Explorer REST query API (`/v1/rest/query` or `/v2/rest/query`) on the configured FinOps hub cluster.
+- **When to use it:** Use this tool to check data freshness for FinOps hub functions such as `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()`. It reports latest data dates, stale functions, and functions without data. Treat `Costs()` as the authoritative freshness signal. If `Costs()` is 3 days old or newer, don't report the hub as stale even if older memory, raw KQL, or ingestion timestamp checks disagree.
+- **Example prompt:** "Check data freshness for the FinOps hub at `` in the hub database and tell me which functions are stale."
+- **Sample output shape:**
+
+ ```json
+ {
+ "cluster_uri": "https://.kusto.windows.net",
+ "database": "hub",
+ "functions": [
+ {
+ "function_name": "Costs()",
+ "source_dataset": "Costs",
+ "expected_export_dataset": "focuscost",
+ "row_count": 100000,
+ "latest_data_date": "2026-05-01T00:00:00Z",
+ "latest_ingestion_time": null,
+ "staleness_days": 1,
+ "is_stale": false,
+ "has_data": true,
+ "schema_status": "ok",
+ "schema_column_count": 120,
+ "required_for_hub_health": true,
+ "diagnostic_code": "OK",
+ "diagnostic_message": null
+ }
+ ],
+ "total_functions": 4,
+ "stale_count": 0,
+ "empty_count": 0,
+ "error_count": 0,
+ "hub_data_stale": false,
+ "attention_required": false,
+ "status": "healthy",
+ "diagnostics": [],
+ "freshest_function": {},
+ "stalest_function": {},
+ "authoritative_freshness_signal": {
+ "function_name": "Costs()",
+ "latest_data_date": "2026-05-01T00:00:00Z",
+ "staleness_days": 1,
+ "is_stale": false,
+ "has_data": true,
+ "row_count": 100000
+ },
+ "source_of_truth": {
+ "tool": "data-freshness-check",
+ "method": "direct_adx_rest_query",
+ "endpoint": "/v2/rest/query",
+ "authoritative_function": "Costs()",
+ "freshness_threshold_days": 3,
+ "supersedes": ["stale memory conclusions", "raw KQL freshness rollups", "Kusto ingestion timestamp checks"]
+ }
+ }
+ ```
+
+
+
+## Advisor
+
+Use Advisor tools when the agent needs to apply governance decisions to Azure Advisor recommendations.
+
+### `suppress-advisor-recommendations`
+
+- **Azure API:** Azure Resource Graph to find Advisor recommendations, then Azure Resource Manager Advisor suppressions API: `Microsoft.Advisor/recommendations/suppressions`.
+- **When to use it:** Use this tool to suppress selected Azure Advisor recommendation types across subscriptions under a management group for a limited time to live.
+- **Example prompt:** "Suppress the default Advisor recommendation types for management group `` for 30 days and report how many recommendations were suppressed."
+- **Sample output shape:**
+
+ ```json
+ {
+ "management_group_id": "",
+ "days": 30,
+ "ttl": "P30D",
+ "recommendation_type_ids": ["89515250-1243-43d1-b4e7-f9437cedffd8"],
+ "found_count": 10,
+ "suppressed_count": 10,
+ "failed_count": 0,
+ "suppressions": [
+ {
+ "subscription_id": "",
+ "recommendation_id": "/subscriptions//providers/Microsoft.Advisor/recommendations/",
+ "recommendation_type_id": "89515250-1243-43d1-b4e7-f9437cedffd8",
+ "suppression_id": "",
+ "ttl": "P30D",
+ "status": "suppressed",
+ "status_code": 201,
+ "error": null
+ }
+ ]
+ }
+ ```
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/PythonTools)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Budgeting](../../framework/quantify/budgeting.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+- [Usage optimization](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Resource Graph](/azure/governance/resource-graph/)
+- [Microsoft Cost Management](/azure/cost-management-billing/costs/)
+- [Azure Advisor](/azure/advisor/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Tools shipped for Azure SRE Agent in the FinOps toolkit](tools.md)
+- [Kusto tools](kusto-tools.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/scheduled-tasks.md b/docs-mslearn/toolkit/sre-agent/scheduled-tasks.md
new file mode 100644
index 000000000..f5afd9396
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/scheduled-tasks.md
@@ -0,0 +1,279 @@
+---
+title: Scheduled tasks (Azure SRE Agent in the FinOps toolkit)
+description: Learn how the FinOps toolkit's scheduled tasks automate daily, weekly, monthly, and quarterly FinOps operating rhythms on Azure SRE Agent.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: concept-article
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to understand the scheduled tasks the FinOps toolkit deploys to Azure SRE Agent so that I can plan recurring cost, capacity, and finance reviews.
+---
+
+# Scheduled tasks (Azure SRE Agent in the FinOps toolkit)
+
+The FinOps toolkit ships scheduled tasks that run recurring FinOps operating-rhythm workflows on Azure SRE Agent. They turn common reviews into autonomous checks that gather data, route work to the right specialist agent, generate charts where the data supports them, and post completed reports to Microsoft Teams when a Teams notification connector is configured.
+
+The template deploys 19 scheduled tasks from `src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/`. These tasks cover daily health checks, weekly optimization and capacity reviews, monthly planning and finance reports, semiannual year-over-year analysis, and quarterly strategy.
+
+
+
+## Daily tasks
+
+Daily tasks run every morning to validate FinOps hub health and monitor Azure capacity signals that feed Planning & Estimating, Architecting & Workload Placement, Usage Optimization, Governance, Policy & Risk, and Automation, Tools & Services. They keep cost and capacity surprises visible before each business day.
+
+The `Agent` column lists the owning agent that runs each task. Some prompts delegate to specialist agents (for example, `ftk-hubs-agent` or `azure-capacity-manager`) for portions of the work; that delegation happens inside the task prompt.
+
+| Task | Agent | Schedule | Description |
+|------|-------|----------|-------------|
+| `HubsHealthCheck` | `finops-practitioner` | Daily at 6:00 AM
`0 6 * * *` | FinOps hub version and data freshness validation |
+| `CapacityDailyMonitor` | `finops-practitioner` | Daily at 6:30 AM
`30 6 * * *` | Daily capacity health check — quota usage, CRG utilization, zone capacity |
+
+
+
+## Weekly tasks
+
+Weekly tasks summarize cost optimization, Azure capacity evidence, and benefit recommendation activity for the previous week. They give the team a recurring rhythm for deeper analysis without manual report requests.
+
+| Task | Agent | Schedule | Description |
+|------|-------|----------|-------------|
+| `ComputeUtilizationTrend` | `finops-practitioner` | Weekly on Monday at 7:00 AM
`0 7 * * 1` | Weekly VM quota utilization trend review across subscriptions and regions |
+| `CostOptimization` | `finops-practitioner` | Weekly on Monday at 8:00 AM
`0 8 * * 1` | Comprehensive cost optimization report with orphaned resources, rightsizing, and commitment analysis |
+| `CapacityWeeklySupplyReview` | `finops-practitioner` | Weekly on Monday at 8:00 AM
`0 8 * * 1` | Weekly capacity evidence review — quota headroom, CRG cost optimization, SKU availability, benefit recommendations |
+| `NonComputeQuotaAudit` | `finops-practitioner` | Weekly on Tuesday at 7:00 AM
`0 7 * * 2` | Weekly audit of storage, network, and non-compute quota usage at risk |
+| `DbQuotaAudit` | `finops-practitioner` | Weekly on Wednesday at 7:00 AM
`0 7 * * 3` | Weekly audit of database quota and region or zone access risks |
+| `SkuAvailabilityAudit` | `finops-practitioner` | Weekly on Wednesday at 7:30 AM
`30 7 * * 3` | Weekly audit of regional SKU availability and restrictions that could block deployments |
+| `MonitoringScopeValidation` | `finops-practitioner` | Weekly on Thursday at 9:00 AM
`0 9 * * 4` | Weekly validation that FinOps hub monitoring covers all active subscriptions |
+| `BenefitRecommendationReview` | `finops-practitioner` | Weekly on Friday at 8:00 AM
`0 8 * * 5` | Weekly review of reservation and savings plan recommendations, with CFO consultation for decision framing |
+
+
+
+## Monthly tasks
+
+Monthly tasks run after billing data finalizes to produce year-to-date analysis, capacity planning forecasts, AI workload cost reviews, and audit reports for budget and alert coverage. Use them to anchor monthly business reviews.
+
+| Task | Agent | Schedule | Description |
+|------|-------|----------|-------------|
+| `StoragePaasGrowthForecast` | `finops-practitioner` | Monthly on the 1st at 8:00 AM
`0 8 1 * *` | Monthly storage and PaaS quota growth forecast across active subscriptions |
+| `AdvisorSuppressionReview` | `finops-practitioner` | Monthly on the 1st at 9:00 AM
`0 9 1 * *` | Monthly review of active Advisor recommendation suppressions for stale or expired decisions |
+| `CapacityMonthlyPlanning` | `finops-practitioner` | Monthly on the 1st at 9:00 AM
`0 9 1 * *` | Monthly capacity planning cycle — demand forecast, capacity request pipeline, governance review |
+| `Monthly` | `finops-practitioner` | Monthly on the 5th at 5:15 PM
`15 17 5 * *` | Autonomous month-over-month cost analysis with FinOps hub tools |
+| `AIWorkloadCostAnalysis` | `finops-practitioner` | Monthly on the 1st at 10:00 AM
`0 10 1 * *` | Monthly AI workload cost analysis — token economics, model efficiency, and cost allocation for Azure OpenAI |
+| `Semiannual` | `finops-practitioner` | January 5 and July 5 at 9:00 AM
`0 9 5 1,7 *` | Semiannual year-over-year finance analysis with forecast and CFO consultation |
+| `BudgetCoverageAudit` | `finops-practitioner` | Monthly on the 15th at 8:00 AM
`0 8 15 * *` | Monthly audit of subscription budget coverage and missing budget controls |
+| `AlertCoverageAudit` | `finops-practitioner` | Monthly on the 16th at 8:00 AM
`0 8 16 * *` | Monthly audit of cost anomaly alert coverage across active subscriptions |
+
+
+
+## Quarterly tasks
+
+Quarterly tasks run at the start of each calendar quarter to summarize capacity-related FinOps capability maturity, commitment alignment, and architecture evolution. Use them to feed leadership reviews and the next quarter's planning cycle.
+
+| Task | Agent | Schedule | Description |
+|------|-------|----------|-------------|
+| `CapacityQuarterlyStrategy` | `finops-practitioner` | Quarterly on January 1, April 1, July 1, and October 1 at 9:00 AM
`0 9 1 1,4,7,10 *` | Quarterly capacity strategy review — FinOps capability maturity, commitment alignment, architecture evolution |
+
+
+
+## Task details
+
+Each scheduled task is defined in YAML under `src/templates/sre-agent/recipes/finops-hub/automations/scheduled-tasks/`. You can customize the schedule by changing the `cron_expression` in the task definition before deployment. You can also tune the task prompt to change thresholds, scope, report sections, and recommended actions for your operating model.
+
+### HubsHealthCheck
+
+- **Data sources queried:** GitHub release metadata for the latest FinOps hub version, Azure resource discovery for deployed hub resources, and the `data-freshness-check` tool for the latest `Costs()` update date. The task also validates hub connectivity before trusting downstream scheduled reports.
+- **Output format and content:** A daily health summary that reports deployed version status, latest available version, data refresh status, connectivity findings, and any blocking hub issues. The report is formatted for Teams when notifications are configured.
+- **Recommended actions generated:** Upgrade FinOps hubs when the deployed version is behind, investigate stale exports or ingestion failures, restore connectivity, and delay cost analysis that depends on stale hub data.
+- **Customization options:** Change the daily cron from `0 6 * * *`, adjust how many days of data freshness lag is acceptable, add organization-specific hub resource filters, and add required escalation steps for stale data or version drift.
+
+### CapacityDailyMonitor
+
+- **Data sources queried:** VM quota usage through `vm-quota-usage`, capacity reservation group data through `capacity-reservation-groups`, non-compute quota context through `non-compute-quotas`, Azure CLI discovery for AKS and regional resources, and hub freshness checks where cost context is needed.
+- **Output format and content:** A daily capacity health report with quota pressure, capacity reservation utilization, AKS node pool readiness, alert status, and urgent capacity blockers mapped to FinOps capability impact.
+- **Recommended actions generated:** File quota increases, redistribute workload demand, adjust capacity reservation groups, investigate underutilized reservations, validate AKS node pool capacity, and escalate imminent deployment blockers.
+- **Customization options:** Change the daily cron from `30 6 * * *`, tune warning and critical quota-utilization thresholds, define target capacity reservation utilization bands, scope monitored subscriptions or regions, and add workload-specific AKS node pool checks.
+
+### Monthly
+
+- **Data sources queried:** `finops-practitioner` delegates FinOps hub Kusto evidence to `ftk-database-query`, including `data-freshness-check`, `monthly-cost-trend`, `monthly-cost-change-percentage`, `top-services-by-cost`, `top-resource-groups-by-cost`, `cost-by-region-trend`, `cost-anomaly-detection`, `savings-summary-report`, `commitment-discount-utilization`, `cost-forecasting-model`, `reservation-recommendation-breakdown`, `top-resource-types-by-cost`, `service-price-benchmarking`, and `top-other-transactions`. Capacity-risk evidence is delegated to `azure-capacity-manager`.
+- **Output format and content:** A daily month-over-month cost analysis with executive summary, service and resource group drivers, anomalies, forecasts, savings and commitment signals, regional distribution, tag coverage, marketplace or other purchases, charts where data supports them, and action items.
+- **Recommended actions generated:** Investigate anomalies, correct cost allocation gaps, prioritize cost drivers, act on savings opportunities, review commitment utilization, address forecast risk, and validate tags or financial hierarchy gaps.
+- **Customization options:** Change the monthly cron from `15 17 5 * *`, adjust anomaly and variance thresholds, change the comparison window, scope the analysis to selected subscriptions or billing entities, and add or remove Kusto sections from the report.
+
+### ComputeUtilizationTrend
+
+- **Data sources queried:** VM quota utilization through `vm-quota-usage`, Azure Resource Graph subscription and VM inventory, and prior-period utilization signals from the scheduled analysis prompt.
+- **Output format and content:** A weekly trend report that lists quota families, subscriptions, regions, current utilization, week-over-week movement, and quota lines approaching defined risk thresholds.
+- **Recommended actions generated:** Request quota increases for growing usage, move demand to lower-pressure regions or quota families, validate planned deployments against headroom, and close stale quota allocations that are no longer needed.
+- **Customization options:** Change the weekly cron from `0 7 * * 1`, tune warning and critical utilization thresholds, choose the trend lookback window, filter to production subscriptions or priority regions, and add business-unit routing for quota owners.
+
+### CostOptimization
+
+- **Data sources queried:** Azure Resource Graph for orphaned and idle resources, Advisor cost recommendations, commitment and benefit recommendation tools, Azure CLI resource validation, optional VM utilization data, and FinOps hub data when available.
+- **Output format and content:** A weekly cost optimization report with executive summary, quick wins, orphaned resources, rightsizing candidates, commitment discount status, effort and risk categories, estimated savings, and charts for major opportunities.
+- **Recommended actions generated:** Delete or remediate orphaned resources, resize underutilized workloads, buy or adjust reservations and savings plans, validate Advisor recommendations, and assign owners for high-value optimization actions.
+- **Customization options:** Change the weekly cron from `0 8 * * 1`, tune savings and utilization thresholds, exclude protected resource groups or tags, change effort and risk categories, and modify recommendation ranking or minimum savings filters.
+
+### CapacityWeeklySupplyReview
+
+- **Data sources queried:** VM quota usage, `capacity-reservation-groups`, `non-compute-quotas`, `sku-availability`, benefit and reservation recommendations, Azure CLI regional discovery, and commitment-related cost signals.
+- **Output format and content:** A weekly capacity evidence review covering quota headroom, capacity reservation waste, SKU and zone availability, region access, non-compute constraints, and capacity-to-rate optimization opportunities mapped to FinOps capabilities.
+- **Recommended actions generated:** Increase quota, rebalance capacity reservations, release or resize underused CRGs, validate alternate SKUs or regions, align capacity reservations with reservation or savings opportunities, and escalate SKU restrictions.
+- **Customization options:** Change the weekly cron from `0 8 * * 1`, tune quota and CRG utilization thresholds, set approved regions and SKU allowlists, scope the review to critical workloads, and adjust how benefit recommendations are ranked.
+
+### NonComputeQuotaAudit
+
+- **Data sources queried:** `non-compute-quotas` for storage, network, and other service quota usage; subscription scope from Azure discovery; and service-specific quota values returned by Azure APIs when available.
+- **Output format and content:** A weekly audit that separates reported and estimated limits, lists services and regions near limits, highlights quotas with missing limit telemetry, and summarizes risk by subscription.
+- **Recommended actions generated:** Request quota increases, reduce or redistribute usage, add missing quota monitoring, validate estimated limits with service owners, and escalate high-risk storage or network quotas before deployments fail.
+- **Customization options:** Change the weekly cron from `0 7 * * 2`, tune at-risk utilization thresholds, choose monitored services, scope subscriptions or regions, and define how to handle quotas that report current usage but not limits.
+
+### DbQuotaAudit
+
+- **Data sources queried:** `db-service-quotas` for SQL DB, SQL Managed Instance, Cosmos DB, PostgreSQL Flexible Server, and MySQL Flexible Server quota, region, and availability-zone access signals.
+- **Output format and content:** A weekly database quota report that separates service limits, regional access, zone access, estimated or missing evidence, and workload impact by subscription and region.
+- **Recommended actions generated:** Request database quota increases, validate region or zone access, adjust database placement plans, add owner routing for capacity risks, and escalate blockers before deployment or scale events.
+- **Customization options:** Change the weekly cron from `0 7 * * 3`, choose database services or regions to inspect, tune risk thresholds, and define owner metadata requirements.
+
+### SkuAvailabilityAudit
+
+- **Data sources queried:** `sku-availability` for regional SKU availability and restrictions, Azure subscription and region discovery, and workload-provided SKU filters when present.
+- **Output format and content:** A weekly availability report that lists requested SKUs by region, availability status, restrictions, deployment blockers, and recommended fallback regions or SKU families.
+- **Recommended actions generated:** Change deployment region or SKU, request regional access where possible, update landing-zone allowed SKU lists, revise deployment plans, and escalate blockers that affect production capacity.
+- **Customization options:** Change the weekly cron from `30 7 * * 3`, tune the SKU and region scope, add required workload SKUs, define preferred fallback regions, and change the severity threshold for restricted or unavailable SKUs.
+
+### MonitoringScopeValidation
+
+- **Data sources queried:** Azure Resource Graph for active subscription inventory, FinOps hub `data-freshness-check`, hub database and cluster configuration, and subscription coverage comparisons between Azure and hub data.
+- **Output format and content:** A weekly monitoring coverage report showing active subscriptions, subscriptions with fresh hub data, subscriptions missing from monitoring, stale coverage, and evidence for scope gaps.
+- **Recommended actions generated:** Add missing subscriptions to exports or hub monitoring, fix stale export configuration, validate hub permissions, update subscription onboarding processes, and pause unsupported financial analysis for missing scopes.
+- **Customization options:** Change the weekly cron from `0 9 * * 4`, tune data freshness thresholds, filter excluded or retired subscriptions, change the required coverage percentage, and add owner routing for missing subscription remediation.
+
+### BenefitRecommendationReview
+
+- **Data sources queried:** `benefit-recommendations` for reservation and savings plan opportunities, billing-scope context, plus Kusto-backed commitment utilization and savings evidence delegated to `ftk-database-query`.
+- **Output format and content:** A weekly executive report with total savings opportunity, recommendation categories, term and scope assumptions, purchase candidates, risk notes, and decision-ready next steps.
+- **Recommended actions generated:** Approve high-confidence purchases, reject or defer recommendations that conflict with demand forecasts, adjust commitment coverage, request validation from workload owners, and document finance approval decisions.
+- **Customization options:** Change the weekly cron from `0 8 * * 5`, tune minimum savings and confidence thresholds, set preferred commitment terms, scope recommendations by billing profile or subscription, and add finance approval rules.
+
+### StoragePaasGrowthForecast
+
+- **Data sources queried:** `non-compute-quotas` for current storage and PaaS quota usage, Azure Resource Graph for service inventory, subscription scope from Azure discovery, and historical growth signals derived from current usage where available.
+- **Output format and content:** A monthly forecast that identifies storage and PaaS services approaching limits, current usage and limit signals, projected growth risk, and subscriptions needing quota planning.
+- **Recommended actions generated:** Request quota increases, archive or clean up unused capacity, move demand to less constrained subscriptions or regions, add service-specific monitoring, and validate growth assumptions with workload owners.
+- **Customization options:** Change the monthly cron from `0 8 1 * *`, tune forecast horizon and risk thresholds, select included PaaS services, define acceptable headroom, and add workload-specific growth assumptions.
+
+### AdvisorSuppressionReview
+
+- **Data sources queried:** Azure Resource Graph for Advisor suppression resources and metadata, subscription inventory, suppression age and expiration fields, and related recommendation context where available.
+- **Output format and content:** A monthly suppression inventory with active, stale, expired, and undocumented suppressions; affected recommendations; age; owner information; and remediation status.
+- **Recommended actions generated:** Remove expired suppressions, renew justified suppressions with documentation, restore important Advisor recommendations, assign owners for stale decisions, and create follow-up work for risky suppressed savings.
+- **Customization options:** Change the monthly cron from `0 9 1 * *`, tune stale-age and expiration thresholds, scope suppression review by subscription or owner tag, add required justification fields, and define exception approval rules.
+
+### CapacityMonthlyPlanning
+
+- **Data sources queried:** `azure-capacity-manager` uses `vm-quota-usage`, `capacity-reservation-groups`, `resource-graph-query`, and benefit recommendations directly, then asks `ftk-database-query` for Kusto-backed forecast, commitment utilization, and savings evidence.
+- **Output format and content:** A monthly capacity planning packet with demand forecast, capacity request pipeline, quota and CRG allocation review, cost impact, governance findings, and planning decisions.
+- **Recommended actions generated:** Submit quota and capacity requests, adjust capacity reservations, align capacity requests with forecasted demand, rebalance allocation across subscriptions, and update governance controls for capacity planning.
+- **Customization options:** Change the monthly cron from `0 9 1 * *`, tune forecast horizon and growth thresholds, choose capacity planning regions and SKUs, set acceptable CRG utilization bands, and add capacity request workflow fields.
+
+### Semiannual
+
+- **Data sources queried:** `finops-practitioner` delegates FinOps hub Kusto evidence to `ftk-database-query`, including `monthly-cost-trend`, `quarterly-cost-by-resource-group`, `cost-anomaly-detection`, `savings-summary-report`, `commitment-discount-utilization`, `cost-by-financial-hierarchy`, `cost-forecasting-model`, `reservation-recommendation-breakdown`, `cost-by-region-trend`, `top-resource-types-by-cost`, `service-price-benchmarking`, `monthly-cost-change-percentage`, `top-commitment-transactions`, and `top-other-transactions`.
+- **Output format and content:** A semiannual year-over-year finance report with spend trend, forward forecast, quarterly trends, service portfolio analysis, anomaly narrative, savings realization, commitment performance, hierarchy views, and executive actions.
+- **Recommended actions generated:** Reforecast budgets, address variance drivers, approve savings actions, adjust commitments, escalate financial hierarchy gaps, and document finance risks before fiscal reviews.
+- **Customization options:** Change the semiannual cron from `0 9 5 1,7 *`, align fiscal calendar assumptions, tune variance and forecast thresholds, scope the report by billing or management group hierarchy, and add finance-specific KPI sections.
+
+### AIWorkloadCostAnalysis
+
+- **Data sources queried:** `finops-practitioner` delegates AI cost Kusto evidence to `ftk-database-query`, including `ai-token-usage-breakdown`, `ai-model-cost-comparison`, `ai-daily-trend`, and `ai-cost-by-application`, plus cost and capacity context from Azure discovery and `azure-capacity-manager` when needed.
+- **Output format and content:** A monthly AI cost report covering token economics, model mix, application allocation, daily trends, input and output ratios, month-over-month AI cost movement, optimization opportunities, and charts.
+- **Recommended actions generated:** Substitute models where quality allows, optimize prompts, rightsize environments, improve application cost allocation, assess commitment coverage, and prioritize workloads with high token cost growth.
+- **Customization options:** Change the monthly cron from `0 10 1 * *`, tune token-cost and growth thresholds, choose model families or applications in scope, add quality or latency guardrails for model substitution, and change chargeback allocation rules.
+
+### BudgetCoverageAudit
+
+- **Data sources queried:** Azure Resource Graph for subscription inventory and budget resources, subscription metadata, and budget configuration details such as amount, period, contact, and scope.
+- **Output format and content:** A monthly budget coverage report with active subscriptions, configured budgets, missing or incomplete budgets, coverage gaps, and budget-control remediation items.
+- **Recommended actions generated:** Create missing budgets, update budget contacts and thresholds, align budget scopes to subscription ownership, retire budgets for inactive subscriptions, and escalate subscriptions without financial guardrails.
+- **Customization options:** Change the monthly cron from `0 8 15 * *`, define required budget coverage thresholds, exclude sandbox or retired subscriptions, set minimum alert percentages, and add owner or cost-center routing.
+
+### AlertCoverageAudit
+
+- **Data sources queried:** Azure Resource Graph for active subscriptions and anomaly alert resources, cost alert configuration, and subscription metadata for ownership and scope validation.
+- **Output format and content:** A monthly alert coverage report that lists subscriptions with and without cost anomaly alerts, stale or incomplete alert configuration, missing contacts, and remediation priority.
+- **Recommended actions generated:** Create missing anomaly alerts, repair alert scopes or contacts, standardize alert thresholds, remove obsolete alerts, and assign owners for unmonitored high-spend subscriptions.
+- **Customization options:** Change the monthly cron from `0 8 16 * *`, tune required alert coverage and severity thresholds, exclude low-risk subscriptions, set required notification contacts, and customize anomaly alert policy standards.
+
+### CapacityQuarterlyStrategy
+
+- **Data sources queried:** VM quota usage, reservation and savings recommendations, Kusto-backed commitment and savings evidence delegated to `ftk-database-query`, Azure Resource Graph, Azure CLI architecture signals, and non-compute quota context.
+- **Output format and content:** A quarterly strategy report covering FinOps capability maturity, commitment alignment, architecture evolution, region and quota strategy, governance health, risk register, and executive recommendations.
+- **Recommended actions generated:** Mature capacity governance, align reservations and savings plans with demand, update regional architecture, address non-compute quota constraints, revise capacity request processes, and set quarterly capacity objectives.
+- **Customization options:** Change the quarterly cron from `0 9 1 1,4,7,10 *`, tune maturity scoring and risk thresholds, set strategic regions and SKU families, align with quarterly business review dates, and add organization-specific governance criteria.
+
+
+
+## Notification behavior
+
+Scheduled tasks deliver final reports through Azure SRE Agent notification connectors. When you configure the Microsoft Teams connector, each task posts only the completed summary to the connected Teams channel. The task prompts explicitly avoid posting intermediate findings so the channel stays focused on ready-to-use results.
+
+The prompts also split what goes to Teams and what goes to the agent knowledge base:
+
+- **Teams** receives financial, status, capacity, and strategy results. This includes charts, executive summaries, action items, savings opportunities, quota risks, and capacity recommendations.
+- **Knowledge base** receives operational learnings only. Examples include tool errors, query patterns, API workarounds, pipeline failure modes, and troubleshooting steps that worked.
+- **Financial detail stays out of memory.** Prompts tell the agents not to save customer financial data, cost amounts, savings numbers, token costs, or similar financial details to the knowledge base.
+
+For connector setup, see [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md#configure-notifications).
+
+
+
+## Roadmap
+
+The deployed template includes the 19 scheduled tasks listed on this page. The repository also includes the detailed current task catalog in [CATALOG.md](https://github.com/microsoft/finops-toolkit/blob/main/src/templates/sre-agent/CATALOG.md).
+
+The catalog is a roadmap for future automation ideas. It includes more than 74 potential daily, weekly, monthly, quarterly, and annual tasks across FinOps, capacity management, FinOps for AI, governance, optimization, benchmarking, and executive reporting. Those catalog entries are planned ideas, not deployed scheduled tasks, unless they are listed in the deployed task catalog above.
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/ScheduledTasks)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+- [Usage optimization](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Azure Monitor](/azure/azure-monitor/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [Azure SRE Agent template reference (FinOps toolkit)](template.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/security.md b/docs-mslearn/toolkit/sre-agent/security.md
new file mode 100644
index 000000000..0b7d89305
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/security.md
@@ -0,0 +1,200 @@
+---
+title: Security and permissions for Azure SRE Agent in the FinOps toolkit
+description: Review the permissions, identities, run modes, and data flows the FinOps toolkit configures on Azure SRE Agent before you deploy it in your environment.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: concept-article
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps hub admin, I want to understand the security and permissions the FinOps toolkit configures on Azure SRE Agent so that I can deploy it with least privilege.
+---
+
+# Security and permissions for Azure SRE Agent in the FinOps toolkit
+
+The FinOps toolkit deployment configures Azure SRE Agent with managed identity, Azure RBAC, Azure Data Explorer permissions, and optional Microsoft Teams or Outlook connectors to run FinOps and capacity workflows. Review these permissions before deployment so you can keep the agent scoped to the data and actions it needs.
+
+> [!IMPORTANT]
+> The agent is designed for least privilege. The template grants read, monitoring, Kusto viewer, and zone mapping permissions. It doesn't grant broad write access to Azure resources.
+
+
+
+## Deployment permissions
+
+To deploy the template, the deploying user or service principal needs one of these permission sets on the target subscription:
+
+- **Owner**
+- **User Access Administrator** and **Contributor**
+
+These permissions are needed because deployment creates a resource group, creates Azure resources, assigns Azure RBAC roles, and assigns the deploying principal the Azure SRE Agent Administrator role on the agent resource.
+
+> [!TIP]
+> Use a dedicated deployment identity for production environments. After deployment, review role assignments and remove any temporary permissions the deployment identity no longer needs.
+
+
+
+## Managed identity permissions
+
+The template creates an Azure SRE Agent with a system-assigned managed identity.
+
+The system-assigned managed identity is the primary identity the template manages. The template uses it for Azure resource operations, action execution, the knowledge graph configuration, and connector setup. The deployment grants the system-assigned managed identity these permissions:
+
+| Role | Scope | What it grants | Why it's needed |
+|------|-------|----------------|-----------------|
+| Reader | Subscription | Read access to Azure resources and metadata | Lets the agent inspect resource configuration, cost scopes, quota context, and related Azure inventory |
+| Reader | Target resource groups | Read access to scoped resource groups | Lets the agent inspect explicitly targeted resources |
+| Monitoring Reader | Target resource groups | Read access to Azure Monitor settings and telemetry | Lets the agent use monitoring context for investigations and health checks |
+| Log Analytics Reader | Target resource groups | Read access to Log Analytics resources | Lets the agent query scoped workspace telemetry when configured |
+| Contributor | Target resource groups when `accessLevel` is `High` | Resource-group write access | Lets remediation tools create or update approved alerts and budgets in targeted scopes |
+| AllDatabasesViewer | Azure Data Explorer cluster | Viewer access across databases in the cluster | Lets the Kusto connector query FinOps hub cost data |
+| SRE Agent Administrator | Agent resource | Administer the Azure SRE Agent resource | Lets the deploying principal manage the agent after deployment |
+
+With this default scope, the managed identity can read Azure resource metadata, monitoring context, and FinOps hub data. It can write approved remediation resources only in explicitly targeted resource groups when `accessLevel` is `High`, and it can send messages or email only through connectors you configure.
+
+
+
+## Data Explorer permissions
+
+The agent reads FinOps hub data through an Azure Data Explorer connector. When you provide the optional cluster name and cluster resource group parameters, the deployment assigns `AllDatabasesViewer` on the target Azure Data Explorer cluster to the system-assigned managed identity.
+
+This permission lets the agent query hub data, including cost, usage, commitment, allocation, anomaly, and forecast datasets exposed through the FinOps toolkit Kusto tools. It doesn't grant database administration permissions.
+
+> [!NOTE]
+> If you don't grant `AllDatabasesViewer` during deployment, a cluster administrator can grant the equivalent viewer permission later. The Kusto connector won't return FinOps hub results until the managed identity has permission on the cluster.
+
+
+
+## Connector permissions
+
+The agent doesn't create Teams or Outlook notification connectors during deployment. You add them interactively in [sre.azure.com](https://sre.azure.com) after deployment.
+
+To configure a Teams or Outlook connector, the configuring user needs **Contributor** on the agent resource group, including these actions:
+
+| Permission | Why it's needed |
+|------------|-----------------|
+| `Microsoft.Web/connections/write` | Creates or updates the connector resource that stores the connection configuration |
+| `Microsoft.Authorization/roleAssignments/write` | Assigns the selected managed identity permission to use the connector resource |
+
+Communication connectors use both OAuth and managed identity:
+
+1. You sign in with a Microsoft 365 account through OAuth.
+2. You select the agent's managed identity.
+3. The connector stores the OAuth token securely in the connector resource.
+4. The agent uses the managed identity at runtime to access the connector resource and send messages or email.
+
+For Teams, the account that signs in must have access to the target channel and the channel's **Get link to channel** URL. The connector can post messages to the configured channel, reply to threads, and read channel messages for context.
+
+
+
+## Run modes
+
+Azure SRE Agent supports two run modes:
+
+| Mode | Security behavior | Use with the FinOps toolkit deployment |
+|------|-------------------|---------------------------|
+| Review | The agent proposes Azure infrastructure write actions, and an SRE Agent Administrator approves or denies them | Best for production response plans and any workflow that might change resources |
+| Autonomous | The agent runs allowed actions without waiting for approval | Best for trusted recurring reports, health checks, and scheduled FinOps summaries |
+
+The template sets the agent action configuration to **Autonomous** so scheduled cost and capacity reports can run and post results without manual approval.
+
+> [!IMPORTANT]
+> Run mode doesn't replace permissions. The agent can act only when its managed identity, connector, and data source permissions allow the action. Review mode adds an approval step for Azure infrastructure write actions, but other actions, such as querying data or posting to Teams, run based on available tools and permissions.
+
+Start with Review for workflows that could affect production resources. Use Autonomous for read-only reporting tasks after you confirm the prompts, tools, and destination channels are correct.
+
+
+
+## B2B tenant considerations
+
+In B2B environments, the Azure subscription and Azure SRE Agent resource can be in a different Microsoft Entra tenant than your Microsoft 365 home tenant.
+
+Use these checks when deployment, Azure MCP Server, or connector configuration fails:
+
+1. Confirm the active Azure CLI context points to the subscription that owns the SRE Agent resource.
+2. Re-authenticate against the tenant that owns the subscription.
+3. When using Azure MCP Server `sreagent` tools, pass the resource tenant, subscription, resource group, and agent name explicitly.
+4. If Azure MCP Server returns `403` from its SRE Agent path, verify the same identity can read the ARM resource and data-plane endpoint before changing agent permissions.
+
+Browser access can succeed while a local tool returns `401`, `403`, or `Forbidden` if the token was issued for the wrong tenant. The deployment script sets the active subscription before deployment to reduce this risk.
+
+Connector setup can also cross identity boundaries. The Azure resource permissions come from the resource tenant, while Teams or Outlook OAuth uses the Microsoft 365 account that signs in to the connector.
+
+
+
+## Data flow
+
+The agent reads operational and cost data from these sources:
+
+- FinOps hub cost and usage data in Azure Data Explorer.
+- Azure resource metadata through Reader permissions.
+- Azure Monitor and Log Analytics context through target resource group reader permissions.
+- Uploaded knowledge documents that describe FinOps toolkit tools, Teams notification patterns, report output style, and known issues.
+
+Scheduled reports and investigation summaries can be sent to:
+
+- The Azure SRE Agent chat experience.
+- A configured Microsoft Teams channel.
+- Outlook, if you add the Outlook connector.
+
+Don't save financial data to agent memory. Save only operational notes, such as tool errors, workarounds, and repeatable patterns.
+
+Data residency follows the Azure resources and Microsoft 365 services you connect. FinOps hub data stays in your Azure Data Explorer cluster until queried. Agent telemetry is written to the deployed Log Analytics and Application Insights resources. Teams or Outlook reports are delivered to the channel or mailbox you configure, and OAuth tokens are stored in the connector resource rather than in the agent prompt or knowledge files.
+
+
+
+## Least privilege checklist
+
+Use this checklist before you move from test to production:
+
+- Grant the deployment identity only the permissions needed for deployment.
+- Keep the agent managed identity scoped to subscription Reader, target resource group reader/remediation permissions, and Azure Data Explorer viewer permissions unless your own workflows require more.
+- Use Review mode for production workflows that can change Azure resources.
+- Use Autonomous mode only for trusted reports, health checks, and read-only tasks.
+- Configure Teams and Outlook connectors with accounts that are allowed to send to the target audience.
+- Send reports only to channels or mailboxes approved for cost and capacity information.
+- Review Data Explorer access when hub databases or clusters change.
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+- [Usage optimization](../../framework/optimize/workloads.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Run modes in Azure SRE Agent](/azure/sre-agent/run-modes)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Azure Monitor](/azure/azure-monitor/)
+
+Related solutions:
+
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Azure SRE Agent template reference (FinOps toolkit)](template.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/template.md b/docs-mslearn/toolkit/sre-agent/template.md
new file mode 100644
index 000000000..80047181f
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/template.md
@@ -0,0 +1,209 @@
+---
+title: Azure SRE Agent template reference (FinOps toolkit)
+description: Review the FinOps toolkit's Azure SRE Agent deployment template, parameters, outputs, script flags, and Bicep module structure.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: reference
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps hub admin, I want to understand the FinOps toolkit's Azure SRE Agent template so that I can deploy and customize it safely.
+---
+
+# Azure SRE Agent template reference (FinOps toolkit)
+
+This reference summarizes the [FinOps toolkit's Azure SRE Agent template](https://github.com/microsoft/finops-toolkit/tree/main/src/templates/sre-agent). Use it to review deployment prerequisites, Bicep parameters, script options, and module structure before you deploy or customize the template.
+
+
+
+## Prerequisites
+
+Ensure the following prerequisites are met before you deploy the template:
+
+
+- You must have permissions to create the deployed resources and assign roles.
+
+ | Task | Minimum permission |
+ | ---- | ------------------ |
+ | Deploy the subscription-scoped Bicep template and create the target resource group | [Contributor](/azure/role-based-access-control/built-in-roles#contributor) on the subscription |
+ | Assign subscription and target resource group roles to the agent managed identity | [Role Based Access Control Administrator](/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator), [User Access Administrator](/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](/azure/role-based-access-control/built-in-roles#owner) on the assignment scopes |
+ | Assign Azure Data Explorer access when cluster parameters are set | Permission to create `Microsoft.Kusto/clusters/principalAssignments` on the target cluster |
+ | Apply Azure SRE Agent objects with the portal deployment script or `bin/apply-extras.sh` | SRE Agent Administrator on the deployed Azure SRE Agent resource |
+
+- The `Microsoft.App` resource provider must be registered in the subscription.
+- For local CLI deployments only: [Azure CLI](/cli/azure/install-azure-cli), `curl`, `jq`, Bash 3.2 or newer, and `python3` with PyYAML must be available locally.
+- A FinOps hub with Azure Data Explorer is required when you want the agent to query hub data.
+
+
+
+
+## Parameters
+
+Here are the parameters you can use to customize the deployment:
+
+| Parameter | Type | Default value | Allowed values | Description |
+| --------- | ---- | ------------- | -------------- | ----------- |
+| **resourceGroupName** | String | None | Any string | Required. Resource group that contains the SRE Agent resources. |
+| **agentName** | String | None | Any string | Required. Azure SRE Agent name. |
+| **location** | String | `eastus2` | `australiaeast`, `canadacentral`, `eastus2`, `francecentral`, `koreacentral`, `swedencentral`, `uksouth` | Optional. Primary location for all resources. |
+| **targetResourceGroups** | Array | `[]` | Resource group names | Optional. Resource groups the agent can observe or act on. Defaults to the agent resource group. |
+| **targetResourceGroupNames** | String | `""` | Comma-separated resource group names | Optional. Portal form input for target resource groups. Defaults to the agent resource group. |
+| **accessLevel** | String | `High` | `Low`, `High` | Optional. Agent access level. |
+| **actionMode** | String | `autonomous` | `review`, `autonomous`, `readOnly` | Optional. Agent action mode. |
+| **upgradeChannel** | String | `Preview` | `Stable`, `Preview` | Optional. Agent upgrade channel. |
+| **defaultModelProvider** | String | `MicrosoftFoundry` | `MicrosoftFoundry`, `Anthropic` | Optional. Parent SRE Agent model provider. `MicrosoftFoundry` maps to Azure OpenAI in the SRE Agent portal. |
+| **defaultModelName** | String | `Automatic` | Any supported model name | Optional. Parent SRE Agent model name. Use `Automatic` so SRE Agent routes to the appropriate model inside the selected provider. |
+| **monthlyAgentUnitLimit** | Int | `10000` | 1 or higher | Optional. Monthly agent unit limit. |
+| **experimentalSettings** | Object | `{ "EnableSandboxGroup": true, "EnableWorkspaceTools": true }` | Object | Optional. Agent sandbox and workspace tool settings for the stable SRE Agent runtime. |
+| **finopsHubKustoConnectorUri** | String | `""` | Database-qualified Kusto URI | Optional. Kusto connector URI for the FinOps hub database, such as `https://cluster.region.kusto.windows.net/Hub`. |
+| **finopsHubKustoClusterResourceId** | String | `""` | Azure resource ID | Optional. Azure Data Explorer cluster resource ID for the FinOps hub role assignment. |
+| **enableSubscriptionReaderRole** | Boolean | `true` | `true`, `false` | Optional. Assigns Reader at subscription scope to the agent managed identity. |
+| **recipePackageUri** | String | Derived from the template URI | Public URI | Optional. URI for the packaged recipe assets used by the portal deployment script. |
+| **forceUpdateTag** | String | Current deployment timestamp | Any string | Optional. Forces the portal deployment script to rerun during redeployment. |
+
+
+
+## Deployment values
+
+The portal template receives values from `createUiDefinition.json`, then runs a subscription-scoped ARM deployment directly from the published toolkit deploy artifacts. `bin/deploy.sh` writes a deployment parameter file under the local SRE Agent deployment cache, then runs `az deployment sub create` directly. Neither path uses Azure Developer CLI environments.
+
+| Value | Source | Description |
+| ----- | ------ | ----------- |
+| **subscription** | `--subscription` | Azure subscription ID. |
+| **resourceGroupName** | `--resource-group` | Resource group that contains the SRE Agent resources. |
+| **agentName** | `--name` | Azure SRE Agent name. |
+| **location** | `--location` | Azure location for the deployment. |
+| **targetResourceGroups** | `--target-resource-group` | Target resource group names. Defaults to the agent resource group. |
+| **enableSubscriptionReaderRole** | Default, unless `--no-subscription-reader` is passed | Whether the deployment assigns subscription-scope Reader to the agent managed identity. |
+| **defaultModelProvider** | `agent.json` | Parent SRE Agent model provider. The FinOps hub recipe defaults to Azure OpenAI provider-level routing with `MicrosoftFoundry`. |
+| **defaultModelName** | `agent.json` | Parent SRE Agent model name. The FinOps hub recipe defaults to `Automatic` model routing. |
+| **finopsHubKustoClusterResourceId** | `--cluster-resource-id` | Optional cluster resource ID used to assign `AllDatabasesViewer`. |
+| **FinOps Hub Kusto connector URI** | `--cluster-uri` | Database-qualified Kusto URI, such as `https://cluster.region.kusto.windows.net/Hub`. |
+
+
+
+## Portal recipe configuration
+
+The Deploy to Azure path packages `azuredeploy.json`, `createUiDefinition.json`, and `sre-agent-recipe.zip` under `docs/deploy/sre-agent//`. The `recipePackageUri` parameter defaults to a file beside the linked template by using the ARM deployment template-link URI. The deployment script downloads that package, reads `extras.json`, and applies the same recipe objects as `bin/apply-extras.sh`.
+
+The deployment script uses a user-assigned managed identity. Bicep grants that identity SRE Agent Administrator on the agent resource before the script runs, then the script uses ARM for connectors and the SRE Agent data plane for built-in tools, knowledge, tools, skills, subagents, and scheduled tasks.
+
+
+
+## Azure Data Explorer network access
+
+When `--cluster-uri` points to an Azure Data Explorer cluster, the deployment inspects the cluster's network access setting before creating the agent. If `publicNetworkAccess` is `Disabled`, the deployment doesn't stop. It still deploys all resources, assigns the agent managed identity `AllDatabasesViewer`, and creates the `finops-hub-kusto` connector.
+
+The deployment prints a warning with the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations) because hosted Azure SRE Agent can't run direct KQL queries against private endpoint ADX clusters. The customer must decide whether to enable public query access for the cluster. Until public query access is enabled or Azure SRE Agent adds private endpoint ADX query support, the connector is expected to remain unhealthy.
+
+
+
+## Outputs
+
+Here are the outputs generated by the deployment:
+
+| Output | Type | Description |
+| ------ | ---- | ----------- |
+| **AZURE_RESOURCE_GROUP** | String | Name of the Azure resource group that contains the SRE Agent resources. |
+| **AZURE_LOCATION** | String | Azure location used for the SRE Agent resources. |
+| **SRE_AGENT_NAME** | String | Name of the deployed Azure SRE Agent resource. |
+| **SRE_AGENT_ENDPOINT** | String | Endpoint of the deployed Azure SRE Agent resource. |
+
+
+
+## Script flags
+
+The template includes Bash scripts for one-shot deployment and extras configuration.
+
+### Deployment wrapper
+
+Use `bin/deploy.sh` to write deterministic ARM parameters, deploy `infra/main.bicep` with Azure CLI, and run `bin/apply-extras.sh`.
+
+Supporting resource names are deterministic for the subscription ID, agent resource group ID, and agent name. Rerunning the script with the same tuple updates the same Log Analytics workspace, Application Insights component, system-assigned managed identity RBAC assignments, and SRE Agent instead of creating timestamped duplicates. The optional `--deploy-name` only names the ARM deployment record and local build directory.
+
+| Bash flag | Required | Description |
+| --------- | -------- | ----------- |
+| `--recipe ` | Yes | Recipe directory. Use `recipes/finops-hub`. |
+| `--subscription ` | Yes | Azure subscription ID. |
+| `--resource-group ` | Yes | Resource group for SRE Agent resources. |
+| `--name ` | Yes | Azure SRE Agent name. |
+| `--location ` | Yes | Azure region. |
+| `--target-resource-group ` | No | Repeatable target resource group. Defaults to `--resource-group`. |
+| `--cluster-uri ` | No | Database-qualified FinOps hub Kusto URI, such as `https://cluster.region.kusto.windows.net/Hub`. |
+| `--cluster-resource-id ` | Optional with `--cluster-uri` for real deployments; required for `--dry-run` | Kusto cluster ARM resource ID for `AllDatabasesViewer`. Real deployments resolve it from `--cluster-uri` when the cluster is in the target subscription. |
+| `--no-subscription-reader` | No | Skips the default subscription-scope Reader assignment. Use only when equivalent read access is granted another way. |
+| `--deploy-name ` | No | Deployment name override. Defaults to a deterministic name from subscription ID, resource group, and agent name. |
+| `--dry-run` | No | Validate inputs and write parameters without Azure calls. |
+| `--force`, `--no-telemetry` | No | Compatibility flags accepted by the wrapper. |
+| `-h`, `--help` | No | Show script help. |
+
+### Extras configuration
+
+Use `bin/apply-extras.sh` to configure the Kusto connector through ARM when requested, upload KnowledgeFile sources, and apply custom agents, skills, tools, and scheduled tasks through the SRE Agent data plane. `bin/post-provision.sh` remains as a deprecated compatibility wrapper that forwards to `bin/apply-extras.sh`.
+
+| Bash flag | Required | Description |
+| --------- | -------- | ----------- |
+| `--endpoint ` | Yes | SRE Agent endpoint returned by deployment. |
+| `--recipe ` | Yes | Recipe directory. |
+| `--subscription ` | Yes | Azure subscription that contains the SRE Agent. |
+| `--resource-group ` | Yes | Resource group that contains the SRE Agent. |
+| `--name ` | Yes | SRE Agent name. |
+| `--build-dir ` | Yes | Working directory for generated extras request and response files. |
+| `--kusto-connector-uri ` | No | Database-qualified Kusto connector URI. |
+
+
+
+## Module structure
+
+The template uses a subscription-scoped entry point and resource group modules:
+
+| File | Scope | Deploys or configures |
+| ---- | ----- | --------------------- |
+| `infra/main.bicep` | Subscription | Creates the target resource group, calls the resource group deployment, assigns subscription and target resource group RBAC, and optionally assigns Azure Data Explorer roles. |
+| `infra/resources.bicep` | Resource group | Orchestrates identity, monitoring, and Azure SRE Agent modules, then surfaces outputs to the subscription deployment. |
+| `infra/modules/monitoring.bicep` | Resource group | Creates the Log Analytics workspace and workspace-based Application Insights component for telemetry. |
+| `infra/modules/sre-agent.bicep` | Resource group | Creates the `Microsoft.App/agents@2026-01-01` resource, configures action mode, model provider, sandbox, and workspace tool settings, assigns SRE Agent Administrator to the deployer, and exposes the agent endpoint for `bin/apply-extras.sh`. |
+| `infra/modules/resource-group-rbac.bicep` | Resource group | Assigns Reader, Monitoring Reader, Log Analytics Reader, and optionally Contributor to the agent system-assigned managed identity. |
+| `infra/modules/kusto-all-databases-viewer-rbac.bicep` | Resource group | Assigns `AllDatabasesViewer` on an existing Azure Data Explorer cluster to the system-assigned managed identity when cluster parameters are provided. |
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+
+Related solutions:
+
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [FinOps hubs](../hubs/finops-hubs-overview.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/tools.md b/docs-mslearn/toolkit/sre-agent/tools.md
new file mode 100644
index 000000000..3ff74be88
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/tools.md
@@ -0,0 +1,186 @@
+---
+title: Tools shipped for Azure SRE Agent in the FinOps toolkit
+description: Review the Kusto and Python tools the FinOps toolkit ships for Azure SRE Agent for cost analysis, anomaly detection, rate optimization, capacity management, and operations.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: reference
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to understand which tools the FinOps toolkit ships for Azure SRE Agent so that I can choose the right agent workflow for cost, capacity, and operations analysis.
+---
+
+# Tools shipped for Azure SRE Agent in the FinOps toolkit
+
+The FinOps toolkit deployment configures Azure SRE Agent with tools that ground agent responses in live Azure and FinOps hub data. [Kusto tools](kusto-tools.md) query the FinOps hub Azure Data Explorer database, and [Python tools](python-tools.md) call Azure APIs through the agent's managed identity.
+
+Use this article as a catalog of the tools included with the template. For deeper implementation details, review the [Kusto tools](kusto-tools.md) and [Python tools](python-tools.md) references.
+
+The template configures 50 tools: 37 Kusto query tools generated from the FinOps hub query catalog and 13 Python tools, as documented in the [Kusto tools](kusto-tools.md) and [Python tools](python-tools.md) references. All Kusto tools are assigned only to `ftk-database-query`; other agents request Kusto evidence from that specialist instead of querying FinOps hub directly.
+
+> [!NOTE]
+> The agent list shows subagents that reference each tool in `recipes/finops-hub/config/subagents`. Tools marked "Not assigned" are included in the tool catalog, but aren't referenced by a subagent configuration. For agent roles and tool usage, see [agents and skills](agents.md).
+
+
+
+## AI cost management
+
+AI cost management tools help agents analyze Azure OpenAI spend, token usage, model efficiency, and application-level allocation, as described in [AI/ML costs](kusto-tools.md#aiml-costs).
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `ai-cost-by-application` | `Kusto` | Breaks down Azure OpenAI costs by application, team, and environment tags for chargeback, showback, and allocation; see [ai-cost-by-application](kusto-tools.md#ai-cost-by-application). | `ftk-database-query` |
+| `ai-daily-trend` | `Kusto` | Shows daily Azure OpenAI cost and token trends for AI cost anomaly detection and forecasting; see [ai-daily-trend](kusto-tools.md#ai-daily-trend). | `ftk-database-query` |
+| `ai-model-cost-comparison` | `Kusto` | Compares Azure OpenAI model cost per 1,000 tokens to identify model efficiency opportunities; see [ai-model-cost-comparison](kusto-tools.md#ai-model-cost-comparison). | `ftk-database-query` |
+| `ai-token-usage-breakdown` | `Kusto` | Breaks down Azure OpenAI token consumption by model version and input or output direction; see [ai-token-usage-breakdown](kusto-tools.md#ai-token-usage-breakdown). | `ftk-database-query` |
+
+
+
+## Cost analysis and reporting
+
+Cost analysis and reporting tools summarize FinOps hub cost data by service, region, resource group, financial hierarchy, transaction type, and time period, as described in [Cost analysis](kusto-tools.md#cost-analysis) and [Price analysis](kusto-tools.md#price-analysis).
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `costs-enriched-base` | `Kusto` | Returns a guarded row-level enriched cost and usage sample for narrow drill-downs; see [costs-enriched-base](kusto-tools.md#costs-enriched-base). Use aggregate tools for full-month, fiscal-period, scheduled report, and tag-coverage rollup requests. | `ftk-database-query` |
+| `cost-by-financial-hierarchy` | `Kusto` | Reports top costs across billing profile, invoice section, team, product, application, and environment; see [cost-by-financial-hierarchy](kusto-tools.md#cost-by-financial-hierarchy). | `ftk-database-query` |
+| `cost-by-region-trend` | `Kusto` | Summarizes effective cost by Azure region to identify regional spend distribution and optimization opportunities; see [cost-by-region-trend](kusto-tools.md#cost-by-region-trend). | `ftk-database-query` |
+| `monthly-cost-change-percentage` | `Kusto` | Calculates month-over-month billed and effective cost changes to spot spikes, drops, and volatility; see [monthly-cost-change-percentage](kusto-tools.md#monthly-cost-change-percentage). | `ftk-database-query` |
+| `monthly-cost-trend` | `Kusto` | Returns billed and effective cost totals by month for trend review and budget analysis; see [monthly-cost-trend](kusto-tools.md#monthly-cost-trend). | `ftk-database-query` |
+| `quarterly-cost-by-resource-group` | `Kusto` | Returns top resource-group cost rows by subscription and month for quarterly reporting windows; see [quarterly-cost-by-resource-group](kusto-tools.md#quarterly-cost-by-resource-group). | `ftk-database-query` |
+| `top-other-transactions` | `Kusto` | Lists top non-usage, non-commitment purchase transactions, such as Marketplace or miscellaneous charges; see [top-other-transactions](kusto-tools.md#top-other-transactions). | `ftk-database-query` |
+| `top-resource-groups-by-cost` | `Kusto` | Returns top resource groups by effective cost for focused reporting and optimization; see [top-resource-groups-by-cost](kusto-tools.md#top-resource-groups-by-cost). | `ftk-database-query` |
+| `top-resource-types-by-cost` | `Kusto` | Returns top resource types by resource count and effective cost to identify costly categories; see [top-resource-types-by-cost](kusto-tools.md#top-resource-types-by-cost). | `ftk-database-query` |
+| `top-services-by-cost` | `Kusto` | Returns top Azure services by effective cost to prioritize service-level optimization; see [top-services-by-cost](kusto-tools.md#top-services-by-cost). | `ftk-database-query` |
+
+
+
+## FinOps KPI scorecard
+
+FinOps KPI scorecard tools are generated from [`src/queries/catalog`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/catalog/) and map directly to query links in [`src/queries/KPI.md`](https://github.com/microsoft/finops-toolkit/tree/main/src/queries/KPI.md). Scheduled monthly, semiannual, health, anomaly, capacity, storage, monitoring, and benefit-review tasks request these tools through `ftk-database-query`.
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `allocation-accuracy-index` | `Kusto` | Measures directly attributed cost as a share of total effective cost. | `ftk-database-query` |
+| `anomaly-detection-rate` | `Kusto` | Measures the share of spend in anomaly-flagged daily buckets. | `ftk-database-query` |
+| `anomaly-variance-total` | `Kusto` | Quantifies unpredicted spend variance for detected anomaly events. | `ftk-database-query` |
+| `commitment-discount-waste` | `Kusto` | Measures unused commitment value as a share of total commitment cost. | `ftk-database-query` |
+| `commitment-utilization-score` | `Kusto` | Computes commitment utilization score by commitment and currency. | `ftk-database-query` |
+| `compute-cost-per-core` | `Kusto` | Calculates effective and hourly compute cost per consumed vCPU core hour. | `ftk-database-query` |
+| `compute-spend-commitment-coverage` | `Kusto` | Measures compute spend covered by commitment discounts. | `ftk-database-query` |
+| `cost-optimization-index` | `Kusto` | Computes a cost optimization index from current recommendations and cost context. | `ftk-database-query` |
+| `cost-per-gb-stored` | `Kusto` | Calculates storage cost per normalized GB-month. | `ftk-database-query` |
+| `cost-visibility-delay` | `Kusto` | Measures cost data visibility delay for data-ingestion KPI reporting. | `ftk-database-query` |
+| `data-update-frequency` | `Kusto` | Measures FinOps hub ingestion update cadence. | `ftk-database-query` |
+| `macc-consumption-vs-commitment` | `Kusto` | Measures Microsoft Azure Consumption Commitment drawdown against commitment. | `ftk-database-query` |
+| `percentage-unallocated-costs` | `Kusto` | Measures the share of cost missing required allocation evidence. | `ftk-database-query` |
+| `percentage-untagged-costs` | `Kusto` | Measures the share of cost on resources with no tags. | `ftk-database-query` |
+| `storage-tier-distribution` | `Kusto` | Summarizes storage cost and GB-month distribution by access-tier bucket. | `ftk-database-query` |
+| `tagging-policy-compliance` | `Kusto` | Measures cost-weighted compliance with required tag keys. | `ftk-database-query` |
+
+
+
+## Anomaly detection and forecasting
+
+Anomaly detection and forecasting tools help agents identify unexpected cost changes, project future spend, and deploy alerting automation, as described in [Anomaly detection](kusto-tools.md#anomaly-detection), [Forecasting](kusto-tools.md#forecasting), and [Budget and alert deployment](python-tools.md#budget-and-alert-deployment).
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `cost-anomaly-detection` | `Kusto` | Detects cost spikes and drops with time-series anomaly detection over a configurable history window; see [cost-anomaly-detection](kusto-tools.md#cost-anomaly-detection). | `ftk-database-query` |
+| `cost-forecasting-model` | `Kusto` | Forecasts future effective cost from historical cost data for budgeting and trend projection; see [cost-forecasting-model](kusto-tools.md#cost-forecasting-model). | `ftk-database-query` |
+| `deploy-anomaly-alert` | `Python` | Creates or updates a Cost Management scheduled action for anomaly detection in a subscription after explicit remediation approval; see [deploy-anomaly-alert](python-tools.md#deploy-anomaly-alert). | `finops-practitioner` |
+| `deploy-bulk-anomaly-alerts` | `Python` | Discovers enabled subscriptions in a management group and deploys anomaly alert scheduled actions per subscription after explicit remediation approval; see [deploy-bulk-anomaly-alerts](python-tools.md#deploy-bulk-anomaly-alerts). | `finops-practitioner` |
+
+
+
+## Rate optimization
+
+Rate optimization tools help agents review reservations, savings plans, pricing benchmarks, commitment transactions, and Advisor recommendation suppression workflows, as described in [Commitment discounts](kusto-tools.md#commitment-discounts), [Price analysis](kusto-tools.md#price-analysis), [Resource analysis](python-tools.md#resource-analysis), and [Advisor](python-tools.md#advisor).
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `benefit-recommendations` | `Python` | Gets Microsoft Cost Management benefit recommendations for savings plans and reserved instances at a billing scope; see [benefit-recommendations](python-tools.md#benefit-recommendations). | `finops-practitioner`, `azure-capacity-manager` |
+| `commitment-discount-utilization` | `Kusto` | Analyzes consumed core hours by commitment discount type, including on-demand usage, for a reporting window; see [commitment-discount-utilization](kusto-tools.md#commitment-discount-utilization). | `ftk-database-query` |
+| `reservation-recommendation-breakdown` | `Kusto` | Analyzes reservation recommendations, savings, break-even dates, normalized sizes, scope, and term details; see [reservation-recommendation-breakdown](kusto-tools.md#reservation-recommendation-breakdown). | `ftk-database-query` |
+| `savings-summary-report` | `Kusto` | Summarizes list cost, effective cost, negotiated savings, commitment savings, total savings, and savings rate; see [savings-summary-report](kusto-tools.md#savings-summary-report). | `ftk-database-query` |
+| `service-price-benchmarking` | `Kusto` | Benchmarks services by list cost, contracted cost, effective cost, negotiated savings, commitment savings, and total savings; see [service-price-benchmarking](kusto-tools.md#service-price-benchmarking). | `ftk-database-query` |
+| `suppress-advisor-recommendations` | `Python` | Suppresses selected Azure Advisor recommendations across subscriptions under a management group with a time to live after explicit remediation approval; see [suppress-advisor-recommendations](python-tools.md#suppress-advisor-recommendations). | `finops-practitioner` |
+| `top-commitment-transactions` | `Kusto` | Returns top non-usage commitment discount purchase transactions for reservation and savings plan review; see [top-commitment-transactions](kusto-tools.md#top-commitment-transactions). | `ftk-database-query` |
+
+
+
+## Capacity management
+
+Capacity management tools help agents inspect quota, capacity reservations, SKU availability, and non-compute service limits before planning or deployment, as described in [Capacity and quota](python-tools.md#capacity-and-quota) and the [azure-capacity-manager](agents.md#azure-capacity-manager) agent reference.
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `capacity-reservation-groups` | `Python` | Lists capacity reservation groups and compares reserved capacity with allocated virtual machines; see [capacity-reservation-groups](python-tools.md#capacity-reservation-groups). | `azure-capacity-manager` |
+| `non-compute-quotas` | `Python` | Checks non-compute service quota usage with provider usage APIs and estimated Resource Graph fallbacks; see [non-compute-quotas](python-tools.md#non-compute-quotas). | `azure-capacity-manager` |
+| `sku-availability` | `Python` | Lists Azure Compute or Azure Data Explorer SKU availability, zones, and regional restriction reasons before planning or deployment; see [sku-availability](python-tools.md#sku-availability). | `azure-capacity-manager`, `ftk-hubs-agent` |
+| `vm-quota-usage` | `Python` | Reports VM family quota usage by region and flags families above 80% or 95% utilization; see [vm-quota-usage](python-tools.md#vm-quota-usage). | `azure-capacity-manager` |
+
+
+
+## Governance and automation
+
+Governance and automation tools help agents deploy budget guardrails and run Resource Graph queries for inventory, configuration, and operational checks, as described in [Budget and alert deployment](python-tools.md#budget-and-alert-deployment) and [Resource analysis](python-tools.md#resource-analysis).
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `deploy-budget` | `Python` | Creates or updates a subscription-level Cost Management budget with notification contacts after explicit remediation approval; see [deploy-budget](python-tools.md#deploy-budget). | `finops-practitioner` |
+| `deploy-bulk-budgets` | `Python` | Discovers enabled subscriptions in a management group and creates or updates a Cost Management budget in each subscription after explicit remediation approval; see [deploy-bulk-budgets](python-tools.md#deploy-bulk-budgets). | `finops-practitioner` |
+| `resource-graph-query` | `Python` | Runs Azure Resource Graph KQL queries across subscriptions for inventory and configuration troubleshooting; see [resource-graph-query](python-tools.md#resource-graph-query). | `finops-practitioner`, `azure-capacity-manager`, `ftk-hubs-agent` |
+
+
+
+## Data ingestion and health
+
+Data ingestion and health tools help agents validate whether FinOps hub data is fresh enough to trust for analysis and reporting, as described in [Hub management](python-tools.md#hub-management).
+
+| Tool | Type | Description | Agents |
+|------|------|-------------|--------|
+| `data-freshness-check` | `Python` | Checks FinOps hub function data freshness through direct Azure Data Explorer REST queries. Treats `Costs()` as the authoritative freshness signal with a three-day threshold and supersedes conflicting stale-memory or raw-KQL conclusions; see [data-freshness-check](python-tools.md#data-freshness-check). | `ftk-hubs-agent` |
+
+
+
+## Give feedback
+
+Let us know how we're doing with a [quick review](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent). We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgent)
+
+
+If you're looking for something specific, [vote for an existing or create a new idea](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc). Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+- [Microsoft Cost Management](/azure/cost-management-billing/costs/)
+
+Related solutions:
+
+- [Azure SRE Agent in the FinOps toolkit](overview.md)
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [FinOps hubs](../hubs/finops-hubs-overview.md)
+
+
diff --git a/docs-mslearn/toolkit/sre-agent/troubleshooting.md b/docs-mslearn/toolkit/sre-agent/troubleshooting.md
new file mode 100644
index 000000000..d11ec0516
--- /dev/null
+++ b/docs-mslearn/toolkit/sre-agent/troubleshooting.md
@@ -0,0 +1,252 @@
+---
+title: Troubleshoot Azure SRE Agent deployments from the FinOps toolkit
+description: Resolve common deployment, tenant, connector, data, and query issues for Azure SRE Agent deployments from the FinOps toolkit.
+author: msbrett
+ms.author: brettwil
+ms.date: 06/04/2026
+ms.topic: how-to
+ms.service: finops
+ms.subservice: finops-toolkit
+ms.reviewer: brettwil
+#customer intent: As a FinOps practitioner, I want to troubleshoot the FinOps toolkit's Azure SRE Agent deployment so that I can restore scheduled cost, capacity, and operations workflows.
+---
+
+# Troubleshoot Azure SRE Agent deployments from the FinOps toolkit
+
+Use this guide when the agent deploys, but Azure MCP Server `sreagent` commands, scheduled tasks, connectors, or data queries don't behave as expected. Start with tenant and deployment checks, then use the known issue sections to match the symptom, cause, and workaround.
+
+
+
+## Troubleshoot B2B tenant environments
+
+In B2B environments, the Azure subscription and Azure SRE Agent resource can live in a different Microsoft Entra tenant than your Microsoft 365 home tenant. If [sre.azure.com](https://sre.azure.com) shows the agent correctly but Azure MCP Server `sreagent` commands return `401`, `403`, `AccessDenied`, or `Forbidden`, treat the issue as tenant selection first.
+
+**Symptom:** Browser access works, but Azure MCP Server `sreagent_agents_list`, `sreagent_agents_get`, `sreagent_agents_tools_list`, or related SRE Agent commands fail with `401`, `403`, `AccessDenied`, or `Forbidden`.
+
+**Cause:** The tool token can be issued for the wrong tenant or the tool can route the SRE Agent discovery call through a tenant-scoped Resource Graph path. The browser session can use your Microsoft 365 home tenant, while the Azure SRE Agent resource belongs to a different tenant.
+
+**Workaround:**
+
+1. Confirm the active Azure CLI context points at the subscription that owns the Azure SRE Agent resource.
+1. Re-authenticate Azure CLI against the tenant that owns the subscription and resource.
+1. Pass the resource tenant, subscription, resource group, and agent name explicitly to Azure MCP Server `sreagent` commands.
+1. Verify the same identity can read the ARM resource with `az resource show` and can call the SRE Agent data-plane endpoint with a token for `https://azuresre.dev`.
+1. If ARM and direct data-plane calls work but Azure MCP Server `sreagent` commands still return `AccessDenied`, capture the timestamp and correlation ID from the tool response and route it as an Azure MCP Server or Azure SRE Agent B2B access issue.
+
+> [!TIP]
+> Browser success with Azure MCP Server failure usually means the agent might be healthy, but the tool credential or SRE Agent discovery path needs tenant-specific verification.
+
+
+
+## Fix common deployment failures
+
+Use these checks after `bin/deploy.sh` or post-provisioning fails.
+
+### Unsupported region
+
+**Symptom:** The Bicep deployment fails during validation or resource creation.
+
+**Cause:** The template only supports `australiaeast`, `canadacentral`, `eastus2`, `francecentral`, `koreacentral`, `swedencentral`, and `uksouth`.
+
+**Workaround:** Redeploy in a supported region.
+
+### Resource provider not registered
+
+**Symptom:** Azure Resource Manager fails to create the Azure SRE Agent resource.
+
+**Cause:** The `Microsoft.App` resource provider isn't registered on the target subscription.
+
+**Workaround:** Register the provider, wait for registration to complete, and rerun deployment.
+
+```bash
+az provider register --namespace Microsoft.App
+```
+
+### Missing deployment permissions
+
+**Symptom:** Deployment or role assignment steps fail with authorization errors.
+
+**Cause:** The deploying user doesn't have enough permissions to create resources or assign roles.
+
+**Workaround:** Use an account with permissions to create resources and assign roles at the affected scopes. Contributor can create the deployed resources; Role Based Access Control Administrator, User Access Administrator, or Owner can assign the required roles. If you configure notification connectors later, make sure the configuring user can write connections and role assignments in the agent resource group.
+
+### Zone mapping API returns 404
+
+**Symptom:** Capacity or zone-mapping checks fail when the agent calls `checkZonePeers`.
+
+**Cause:** The `AvailabilityZonePeering` feature isn't registered for the subscription or management group scope.
+
+**Workaround:** Register the feature and re-register the resource provider.
+
+```bash
+az feature register --namespace Microsoft.Resources --name AvailabilityZonePeering
+az provider register --namespace Microsoft.Resources
+```
+
+### Post-provision hook fails
+
+**Symptom:** Azure resources deploy, but skills, agents, tools, scheduled tasks, knowledge documents, or the Kusto connector don't appear in [sre.azure.com](https://sre.azure.com).
+
+**Cause:** The post-provision step couldn't resolve the endpoint, get an SRE Agent data-plane token, configure the Kusto connector, or apply the SRE configuration through the supported ARM and data-plane APIs.
+
+**Workaround:** Check that Azure CLI, `python3`, `jq`, `curl`, and `bash` are available locally. Then rerun `bin/apply-extras.sh` from `src/templates/sre-agent`.
+
+### Connector setup is missing
+
+**Symptom:** Scheduled tasks run, but Teams or Outlook delivery doesn't work.
+
+**Cause:** `bin/deploy.sh` does not create notification connectors because Teams and Outlook require interactive OAuth setup.
+
+**Workaround:** Add the Teams or Outlook connector in [sre.azure.com](https://sre.azure.com), select the agent managed identity, and send a test message.
+
+### Kusto connector is unhealthy for a private endpoint cluster
+
+**Symptom:** The SRE Agent deployment succeeds and the `finops-hub-kusto` connector is created, but the connector doesn't become healthy. The deployment output may warn that the Azure Data Explorer cluster denies public query access.
+
+**Cause:** Hosted Azure SRE Agent runs outside your VNET. Azure SRE Agent can use ARM for resource discovery, health, metrics, and management operations, but direct KQL queries to private endpoint Azure Data Explorer clusters are a documented limitation when `publicNetworkAccess` is `Disabled`.
+
+**Workaround:** Review the [Azure SRE Agent VNET known limitations](https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations), then decide whether to enable public query access for the Azure Data Explorer cluster. The FinOps toolkit deployment doesn't change customer cluster networking automatically. Until public query access is enabled or Azure SRE Agent adds private endpoint ADX query support, Kusto tools that depend on `finops-hub-kusto` are expected to fail while ARM-based health and metrics operations continue to work.
+
+
+
+## Review known issues
+
+The following issues were observed during scheduled task testing. They don't always indicate a broken deployment.
+
+### Teams tool discovery
+
+**Symptom:** A subagent reports that `PostTeamsChannelMessage` isn't available or that it couldn't find the Teams posting function.
+
+**Cause:** Subagents invoked directly for manual testing might not inherit Teams connector tools. Connector tools are available to the base agent or when the platform triggers a scheduled task.
+
+**Workaround:**
+
+- Use the platform cron schedule for production scheduled tasks.
+- For manual testing, invoke the base agent without the `--agent` flag, then delegate with `@subagent` in the prompt.
+- Use the built-in `PostTeamsMessage` tool. Don't call Microsoft Graph or dynamic invoke endpoints directly because that path can return `403`.
+
+### Data pipeline staleness
+
+**Symptom:** Reports show incomplete or missing recent cost data, and forecasts look distorted.
+
+**Cause:** The FinOps hub data pipeline or Cost Management export may be stale, so the Azure Data Explorer cluster might not have current data. Older memory notes, raw KQL checks, or ingestion timestamp checks can also report stale data incorrectly after the pipeline has recovered.
+
+**Workaround:**
+
+- Run `data-freshness-check` or the hubs health check task to confirm freshness before escalating. Treat the direct REST `Costs()` result as the source of truth.
+- If `Costs()` is 3 days old or newer, mark conflicting stale-memory, raw-KQL, or ingestion timestamp conclusions as superseded and don't report the hub as stale.
+- If `Costs()` has no rows, has a query error, or is more than 3 days old, let reports call out stale data clearly and check Cost Management exports in the Azure portal and pipeline runs in Azure Data Factory.
+
+### Resource Graph failures
+
+**Symptom:** `az graph query` returns an unknown error, and Resource Graph-based analysis fails.
+
+**Cause:** The managed identity may not have Reader permissions at the right scope, or complex query expressions may fail in the code interpreter shell environment.
+
+**Workaround:**
+
+- Fall back to scoped `az resource list` queries against specific subscriptions.
+- Confirm the agent managed identity has Reader at the management group or subscription scope.
+- Simplify query expressions.
+
+### Quota CLI failures
+
+**Symptom:** `az quota usage list` or `az vm list-usage` fails in the agent execution environment.
+
+**Cause:** The `az quota` extension might be missing, or the managed identity might not have permission to read quota data.
+
+**Workaround:**
+
+- Use `az vm list-usage --location ` as a compute quota fallback.
+- For broader quota checks, use Azure Resource Manager REST calls from code interpreter.
+- Track persistent CLI failures so the quota tool can be updated.
+
+### JMESPath escaping
+
+**Symptom:** Azure CLI commands fail when `--query` uses backticks, brackets, or property names with dots.
+
+**Cause:** Shell escaping conflicts with JMESPath syntax in the code interpreter environment.
+
+**Workaround:**
+
+- Prefer `--output json`, then parse the result with Python.
+- Use only simple JMESPath selections when you need `--query`.
+- Avoid nested expressions that rely on backtick-escaped property names.
+
+### Memory file conflicts
+
+**Symptom:** A scheduled task returns `File write failed` and says a memory file already exists.
+
+**Cause:** A repeated task run tried to create the same memory file again. The memory system requires an edit operation for existing files.
+
+**Workaround:**
+
+- Use an edit operation for subsequent writes to the same memory file.
+- Treat the first failure as recoverable. Agents can usually switch from create to edit and continue.
+
+### Kusto query errors
+
+**Symptom:** A tool returns `Error executing query on cluster`.
+
+**Cause:** A Kusto tool may reference a function, table, or column that doesn't exist in your FinOps hub version, or the query syntax may need a version-specific update.
+
+**Workaround:**
+
+- Try a simpler query.
+- Check the FinOps hub version.
+- Capture the failing tool name and query so the tool YAML can be fixed.
+
+
+
+## Get support
+
+If the workaround doesn't resolve the issue, [open a GitHub issue](https://github.com/microsoft/finops-toolkit/issues/new/choose) and include:
+
+- The deployment region and target subscription tenant
+- The failing command, task, or tool name
+- The exact error message
+- Whether [sre.azure.com](https://sre.azure.com) can open the agent successfully
+- Whether the issue affects deployment, Azure MCP Server `sreagent` commands, scheduled tasks, connectors, or FinOps hub data
+
+For product ideas or known gaps, [vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc).
+
+
+
+## Give feedback
+
+Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources.
+
+
+> [!div class="nextstepaction"]
+> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20SRE%20Agent%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20SRE%20Agent%3F/surveyId/FTK/bladeName/SREAgent/featureName/SREAgentTroubleshooting)
+
+
+If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes.
+
+
+> [!div class="nextstepaction"]
+> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22Tool%3A%20SRE%20Agent%22%20sort%3Areactions-%2B1-desc)
+
+
+
+
+## Related content
+
+Related FinOps capabilities:
+
+- [Reporting and analytics](../../framework/understand/reporting.md)
+- [Anomaly management](../../framework/understand/anomalies.md)
+- [Rate optimization](../../framework/optimize/rates.md)
+
+Related products:
+
+- [Azure SRE Agent](/azure/sre-agent/overview)
+- [Azure Data Explorer](/azure/data-explorer/)
+
+Related solutions:
+
+- [Deploy Azure SRE Agent with the FinOps toolkit](deploy.md)
+- [FinOps hubs](../hubs/finops-hubs-overview.md)
+- [Azure SRE Agent template reference (FinOps toolkit)](template.md)
+
+
diff --git a/docs/deploy/sre-agent/14.0/azuredeploy.json b/docs/deploy/sre-agent/14.0/azuredeploy.json
new file mode 100644
index 000000000..4b6a6d0eb
--- /dev/null
+++ b/docs/deploy/sre-agent/14.0/azuredeploy.json
@@ -0,0 +1,1119 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "2403415415579441400"
+ }
+ },
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "metadata": {
+ "description": "Resource group that holds the SRE Agent resources."
+ }
+ },
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "location": {
+ "type": "string",
+ "defaultValue": "eastus2",
+ "allowedValues": [
+ "australiaeast",
+ "canadacentral",
+ "eastus2",
+ "francecentral",
+ "koreacentral",
+ "swedencentral",
+ "uksouth"
+ ],
+ "metadata": {
+ "description": "Primary location for all resources."
+ }
+ },
+ "targetResourceGroups": {
+ "type": "array",
+ "defaultValue": [],
+ "metadata": {
+ "description": "Resource groups the agent can observe or act on. The agent resource group is always included."
+ }
+ },
+ "targetResourceGroupNames": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Comma-separated resource groups the agent can observe or act on. Used by the Azure portal form."
+ }
+ },
+ "finopsHubKustoConnectorUri": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional database-qualified FinOps Hub Kusto connector URI. Example: https://..kusto.windows.net/Hub"
+ }
+ },
+ "finopsHubKustoClusterResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "defaultValue": "High",
+ "allowedValues": [
+ "Low",
+ "High"
+ ],
+ "metadata": {
+ "description": "Agent access level."
+ }
+ },
+ "actionMode": {
+ "type": "string",
+ "defaultValue": "autonomous",
+ "allowedValues": [
+ "review",
+ "autonomous",
+ "readOnly"
+ ],
+ "metadata": {
+ "description": "Agent action mode."
+ }
+ },
+ "upgradeChannel": {
+ "type": "string",
+ "defaultValue": "Preview",
+ "allowedValues": [
+ "Stable",
+ "Preview"
+ ],
+ "metadata": {
+ "description": "Agent upgrade channel."
+ }
+ },
+ "defaultModelProvider": {
+ "type": "string",
+ "defaultValue": "MicrosoftFoundry",
+ "allowedValues": [
+ "MicrosoftFoundry",
+ "Anthropic"
+ ],
+ "metadata": {
+ "description": "Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal."
+ }
+ },
+ "defaultModelName": {
+ "type": "string",
+ "defaultValue": "Automatic",
+ "metadata": {
+ "description": "Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider."
+ }
+ },
+ "monthlyAgentUnitLimit": {
+ "type": "int",
+ "defaultValue": 10000,
+ "minValue": 1,
+ "metadata": {
+ "description": "Monthly agent unit limit."
+ }
+ },
+ "experimentalSettings": {
+ "type": "object",
+ "defaultValue": {
+ "EnableSandboxGroup": true,
+ "EnableWorkspaceTools": true
+ },
+ "metadata": {
+ "description": "Agent experimental settings."
+ }
+ },
+ "enableSubscriptionReaderRole": {
+ "type": "bool",
+ "defaultValue": true,
+ "metadata": {
+ "description": "Assign Reader on the deployment subscription to the agent managed identity."
+ }
+ },
+ "recipePackageUri": {
+ "type": "string",
+ "defaultValue": "[uri(deployment().properties.templateLink.uri, 'sre-agent-recipe.zip')]",
+ "metadata": {
+ "description": "Public URI for the generated SRE Agent recipe package. Deploy-to-Azure links derive this from the template URI."
+ }
+ },
+ "forceUpdateTag": {
+ "type": "string",
+ "defaultValue": "[utcNow()]",
+ "metadata": {
+ "description": "Forces the recipe deployment script to run when the template is redeployed."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {
+ "finops-toolkit": "sre-agent",
+ "source": "microsoft-finops-toolkit"
+ },
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "copy": [
+ {
+ "name": "targetRgIds",
+ "count": "[length(variables('targetRgs'))]",
+ "input": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex('targetRgIds')])]"
+ }
+ ],
+ "rawTargetResourceGroups": "[split(replace(parameters('targetResourceGroupNames'), ' ', ''), ',')]",
+ "parsedTargetResourceGroups": "[filter(variables('rawTargetResourceGroups'), lambda('rgName', not(empty(lambdaVariables('rgName')))))]",
+ "targetRgs": "[union(createArray(parameters('resourceGroupName')), parameters('targetResourceGroups'), variables('parsedTargetResourceGroups'))]",
+ "agentResourceGroupId": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]",
+ "namingSeed": "[toLower(format('{0}|{1}|{2}', subscription().subscriptionId, variables('agentResourceGroupId'), parameters('agentName')))]",
+ "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
+ "hasKustoCluster": "[not(empty(parameters('finopsHubKustoClusterResourceId')))]",
+ "kustoClusterSubscriptionId": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[2], '')]",
+ "kustoClusterResourceGroupName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[4], '')]",
+ "kustoClusterName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[8], '')]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "apiVersion": "2024-03-01",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "condition": "[parameters('enableSubscriptionReaderRole')]",
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "name": "[guid(subscription().id, variables('namingSeed'), variables('readerRoleId'))]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('readerRoleId'))]",
+ "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]",
+ "principalType": "ServicePrincipal"
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "resources-deployment",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "agentName": {
+ "value": "[parameters('agentName')]"
+ },
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "namingSeed": {
+ "value": "[variables('namingSeed')]"
+ },
+ "targetResourceGroupIds": {
+ "value": "[variables('targetRgIds')]"
+ },
+ "accessLevel": {
+ "value": "[parameters('accessLevel')]"
+ },
+ "actionMode": {
+ "value": "[parameters('actionMode')]"
+ },
+ "upgradeChannel": {
+ "value": "[parameters('upgradeChannel')]"
+ },
+ "defaultModelProvider": {
+ "value": "[parameters('defaultModelProvider')]"
+ },
+ "defaultModelName": {
+ "value": "[parameters('defaultModelName')]"
+ },
+ "monthlyAgentUnitLimit": {
+ "value": "[parameters('monthlyAgentUnitLimit')]"
+ },
+ "experimentalSettings": {
+ "value": "[parameters('experimentalSettings')]"
+ },
+ "tags": {
+ "value": "[parameters('tags')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "11748842726639098374"
+ }
+ },
+ "parameters": {
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for all resources."
+ }
+ },
+ "namingSeed": {
+ "type": "string",
+ "metadata": {
+ "description": "Deterministic naming seed built from subscription ID, agent resource group ID, and agent name."
+ }
+ },
+ "targetResourceGroupIds": {
+ "type": "array",
+ "metadata": {
+ "description": "Resource group IDs shown as managed resources in the SRE Agent."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent access level."
+ }
+ },
+ "actionMode": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent action mode."
+ }
+ },
+ "upgradeChannel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent upgrade channel."
+ }
+ },
+ "defaultModelProvider": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model provider."
+ }
+ },
+ "defaultModelName": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model name."
+ }
+ },
+ "monthlyAgentUnitLimit": {
+ "type": "int",
+ "metadata": {
+ "description": "Monthly agent unit limit."
+ }
+ },
+ "experimentalSettings": {
+ "type": "object",
+ "metadata": {
+ "description": "Agent experimental settings."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "uniqueSuffix": "[uniqueString(parameters('namingSeed'))]",
+ "logAnalyticsName": "[format('law-{0}', variables('uniqueSuffix'))]",
+ "appInsightsName": "[format('appi-{0}', variables('uniqueSuffix'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "monitoring",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "logAnalyticsName": {
+ "value": "[variables('logAnalyticsName')]"
+ },
+ "appInsightsName": {
+ "value": "[variables('appInsightsName')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "9164683275614055037"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for resources"
+ }
+ },
+ "logAnalyticsName": {
+ "type": "string",
+ "metadata": {
+ "description": "Log Analytics Workspace name"
+ }
+ },
+ "appInsightsName": {
+ "type": "string",
+ "metadata": {
+ "description": "Application Insights name"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.OperationalInsights/workspaces",
+ "apiVersion": "2023-09-01",
+ "name": "[parameters('logAnalyticsName')]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "sku": {
+ "name": "PerGB2018"
+ },
+ "retentionInDays": 30
+ }
+ },
+ {
+ "type": "Microsoft.Insights/components",
+ "apiVersion": "2020-02-02",
+ "name": "[parameters('appInsightsName')]",
+ "location": "[parameters('location')]",
+ "kind": "web",
+ "properties": {
+ "Application_Type": "web",
+ "Request_Source": "IbizaAIExtension",
+ "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "logAnalyticsWorkspaceId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]"
+ },
+ "logAnalyticsWorkspaceName": {
+ "type": "string",
+ "value": "[parameters('logAnalyticsName')]"
+ },
+ "appInsightsId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]"
+ },
+ "appInsightsAppId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').AppId]"
+ },
+ "appInsightsConnectionString": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "sre-agent",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "agentName": {
+ "value": "[parameters('agentName')]"
+ },
+ "appInsightsAppId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsAppId.value]"
+ },
+ "appInsightsConnectionString": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]"
+ },
+ "appInsightsId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsId.value]"
+ },
+ "managedResourceGroupIds": {
+ "value": "[parameters('targetResourceGroupIds')]"
+ },
+ "accessLevel": {
+ "value": "[parameters('accessLevel')]"
+ },
+ "actionMode": {
+ "value": "[parameters('actionMode')]"
+ },
+ "upgradeChannel": {
+ "value": "[parameters('upgradeChannel')]"
+ },
+ "defaultModelProvider": {
+ "value": "[parameters('defaultModelProvider')]"
+ },
+ "defaultModelName": {
+ "value": "[parameters('defaultModelName')]"
+ },
+ "monthlyAgentUnitLimit": {
+ "value": "[parameters('monthlyAgentUnitLimit')]"
+ },
+ "experimentalSettings": {
+ "value": "[parameters('experimentalSettings')]"
+ },
+ "tags": {
+ "value": "[parameters('tags')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "6562535881153200016"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for resources."
+ }
+ },
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "appInsightsAppId": {
+ "type": "string",
+ "metadata": {
+ "description": "Application Insights App ID."
+ }
+ },
+ "appInsightsConnectionString": {
+ "type": "securestring",
+ "metadata": {
+ "description": "Application Insights connection string."
+ }
+ },
+ "appInsightsId": {
+ "type": "string",
+ "metadata": {
+ "description": "Application Insights resource ID."
+ }
+ },
+ "managedResourceGroupIds": {
+ "type": "array",
+ "metadata": {
+ "description": "Resource group IDs to add as managed resources."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent access level."
+ }
+ },
+ "actionMode": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent action mode."
+ }
+ },
+ "upgradeChannel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent upgrade channel."
+ }
+ },
+ "defaultModelProvider": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model provider."
+ }
+ },
+ "defaultModelName": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model name."
+ }
+ },
+ "monthlyAgentUnitLimit": {
+ "type": "int",
+ "metadata": {
+ "description": "Monthly agent unit limit."
+ }
+ },
+ "experimentalSettings": {
+ "type": "object",
+ "metadata": {
+ "description": "Agent experimental settings."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.App/agents",
+ "apiVersion": "2026-01-01",
+ "name": "[parameters('agentName')]",
+ "location": "[parameters('location')]",
+ "tags": "[union(parameters('tags'), createObject('hidden-link: /app-insights-resource-id', parameters('appInsightsId'), 'source', 'microsoft-sre-agent-starter-lab', 'finops-toolkit', 'sre-agent'))]",
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "properties": {
+ "knowledgeGraphConfiguration": {
+ "managedResources": "[parameters('managedResourceGroupIds')]",
+ "identity": "system"
+ },
+ "actionConfiguration": {
+ "mode": "[parameters('actionMode')]",
+ "identity": "system",
+ "accessLevel": "[parameters('accessLevel')]"
+ },
+ "upgradeChannel": "[parameters('upgradeChannel')]",
+ "defaultModel": {
+ "provider": "[parameters('defaultModelProvider')]",
+ "name": "[parameters('defaultModelName')]"
+ },
+ "monthlyAgentUnitLimit": "[parameters('monthlyAgentUnitLimit')]",
+ "experimentalSettings": "[parameters('experimentalSettings')]",
+ "mcpServers": [],
+ "logConfiguration": {
+ "applicationInsightsConfiguration": {
+ "appId": "[parameters('appInsightsAppId')]",
+ "connectionString": "[parameters('appInsightsConnectionString')]"
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]",
+ "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), deployer().objectId, variables('sreAgentAdminRoleId'))]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]",
+ "principalId": "[deployer().objectId]",
+ "principalType": "User"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.App/agents', parameters('agentName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "agentName": {
+ "type": "string",
+ "value": "[parameters('agentName')]"
+ },
+ "agentId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.App/agents', parameters('agentName'))]"
+ },
+ "agentEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01').agentEndpoint]"
+ },
+ "agentPortalUrl": {
+ "type": "string",
+ "value": "[format('https://sre.azure.com/#/agent/{0}/{1}/{2}', subscription().subscriptionId, resourceGroup().name, parameters('agentName'))]"
+ },
+ "agentPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.principalId]"
+ },
+ "agentTenantId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.tenantId]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', 'monitoring')]"
+ ]
+ }
+ ],
+ "outputs": {
+ "agentName": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentName.value]"
+ },
+ "agentEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentEndpoint.value]"
+ },
+ "agentPortalUrl": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPortalUrl.value]"
+ },
+ "agentPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "agentTenantId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentTenantId.value]"
+ },
+ "logAnalyticsWorkspaceId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]"
+ ]
+ },
+ {
+ "copy": {
+ "name": "targetRbac",
+ "count": "[length(variables('targetRgs'))]"
+ },
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('target-rbac-{0}', uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex()])), variables('namingSeed')))]",
+ "resourceGroup": "[variables('targetRgs')[copyIndex()]]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "principalId": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "accessLevel": {
+ "value": "[parameters('accessLevel')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "6305641259472431615"
+ }
+ },
+ "parameters": {
+ "principalId": {
+ "type": "string",
+ "metadata": {
+ "description": "Principal ID of the managed identity to assign roles to."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "allowedValues": [
+ "Low",
+ "High"
+ ],
+ "metadata": {
+ "description": "Agent access level."
+ }
+ }
+ },
+ "variables": {
+ "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
+ "monitoringReaderRoleId": "43d0d8ad-25c7-4714-9337-8ba259a9fe05",
+ "logAnalyticsReaderRoleId": "73c42c96-874c-492b-b04d-ab87d138a893",
+ "contributorRoleId": "b24988ac-6180-42a0-ab88-20f7382dd24c",
+ "roleIds": "[if(equals(parameters('accessLevel'), 'High'), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId'), variables('contributorRoleId')), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId')))]"
+ },
+ "resources": [
+ {
+ "copy": {
+ "name": "roleAssignments",
+ "count": "[length(variables('roleIds'))]"
+ },
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "name": "[guid(resourceGroup().id, parameters('principalId'), variables('roleIds')[copyIndex()])]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleIds')[copyIndex()])]",
+ "principalId": "[parameters('principalId')]",
+ "principalType": "ServicePrincipal"
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]"
+ ]
+ },
+ {
+ "condition": "[variables('hasKustoCluster')]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('kusto-rbac-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed')))]",
+ "subscriptionId": "[variables('kustoClusterSubscriptionId')]",
+ "resourceGroup": "[variables('kustoClusterResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "clusterName": {
+ "value": "[variables('kustoClusterName')]"
+ },
+ "principalApplicationId": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "principalTenantId": {
+ "value": "[tenant().tenantId]"
+ },
+ "principalAssignmentName": {
+ "value": "[format('sre-agent-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed'), 'all-db-viewer'))]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "15341408879505554256"
+ }
+ },
+ "parameters": {
+ "clusterName": {
+ "type": "string",
+ "metadata": {
+ "description": "Kusto cluster name."
+ }
+ },
+ "principalApplicationId": {
+ "type": "string",
+ "metadata": {
+ "description": "Microsoft Entra application/client ID to assign to the Kusto cluster."
+ }
+ },
+ "principalTenantId": {
+ "type": "string",
+ "metadata": {
+ "description": "Principal tenant ID."
+ }
+ },
+ "principalAssignmentName": {
+ "type": "string",
+ "metadata": {
+ "description": "Stable principal assignment name."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Kusto/clusters/principalAssignments",
+ "apiVersion": "2024-04-13",
+ "name": "[format('{0}/{1}', parameters('clusterName'), parameters('principalAssignmentName'))]",
+ "properties": {
+ "principalId": "[parameters('principalApplicationId')]",
+ "principalType": "App",
+ "role": "AllDatabasesViewer",
+ "tenantId": "[parameters('principalTenantId')]"
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "apply-extras",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "agentName": {
+ "value": "[parameters('agentName')]"
+ },
+ "agentEndpoint": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]"
+ },
+ "subscriptionId": {
+ "value": "[subscription().subscriptionId]"
+ },
+ "recipePackageUri": {
+ "value": "[parameters('recipePackageUri')]"
+ },
+ "kustoConnectorUri": {
+ "value": "[parameters('finopsHubKustoConnectorUri')]"
+ },
+ "forceUpdateTag": {
+ "value": "[parameters('forceUpdateTag')]"
+ },
+ "tags": {
+ "value": "[parameters('tags')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "5168893811325309322"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for the deployment script resources."
+ }
+ },
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "agentEndpoint": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent data-plane endpoint."
+ }
+ },
+ "subscriptionId": {
+ "type": "string",
+ "metadata": {
+ "description": "Subscription that contains the SRE Agent."
+ }
+ },
+ "recipePackageUri": {
+ "type": "string",
+ "metadata": {
+ "description": "Public URI for the generated SRE Agent recipe package."
+ }
+ },
+ "kustoConnectorUri": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional database-qualified Kusto connector URI."
+ }
+ },
+ "forceUpdateTag": {
+ "type": "string",
+ "metadata": {
+ "description": "Forces the deployment script to run when the template is redeployed."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "$fxv#0": "# Copyright (c) Microsoft Corporation.\n# Licensed under the MIT License.\n\n$ErrorActionPreference = 'Stop'\n\nfunction Get-RequiredEnv($Name) {\n $value = [Environment]::GetEnvironmentVariable($Name)\n if ([string]::IsNullOrWhiteSpace($value)) {\n throw \"Missing required environment variable: $Name\"\n }\n return $value\n}\n\nfunction Get-OptionalEnv($Name) {\n return [Environment]::GetEnvironmentVariable($Name)\n}\n\nfunction ConvertTo-BodyJson($Value) {\n return ($Value | ConvertTo-Json -Depth 100 -Compress)\n}\n\nfunction Get-PropertyValue($Object, [string[]]$Names, $Default = '') {\n foreach ($name in $Names) {\n if ($null -ne $Object -and $Object.PSObject.Properties[$name] -and $null -ne $Object.$name) {\n return $Object.$name\n }\n }\n return $Default\n}\n\nfunction Get-HttpStatusCode($Exception) {\n if ($Exception.Response -and $Exception.Response.StatusCode) {\n return [int]$Exception.Response.StatusCode\n }\n return $null\n}\n\nfunction Invoke-WithRetry([scriptblock]$Action, [string]$Label, [int]$MaxAttempts = 5, [int]$DelaySeconds = 15) {\n for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {\n try {\n return & $Action\n }\n catch {\n $statusCode = Get-HttpStatusCode $_.Exception\n if ($statusCode -and $statusCode -notin @(401, 403, 408, 429, 500, 502, 503, 504)) {\n throw\n }\n if ($attempt -eq $MaxAttempts) {\n throw\n }\n Write-Output \"$Label attempt $attempt/$MaxAttempts failed: $($_.Exception.Message)\"\n Start-Sleep -Seconds $DelaySeconds\n }\n }\n}\n\nfunction Invoke-JsonRest([string]$Method, [string]$Uri, [string]$Token, $Body = $null) {\n $headers = @{\n Authorization = \"Bearer $Token\"\n 'Content-Type' = 'application/json'\n }\n $parameters = @{\n Method = $Method\n Uri = $Uri\n Headers = $headers\n }\n if ($null -ne $Body) {\n $parameters.Body = ConvertTo-BodyJson $Body\n }\n return Invoke-RestMethod @parameters\n}\n\nfunction Get-KnowledgeSourceName([string]$FileName) {\n $name = [IO.Path]::GetFileName($FileName).ToLowerInvariant() -replace '[^a-z0-9-]+', '-'\n $name = $name -replace '-+', '-'\n return $name.Trim('-')\n}\n\nfunction Get-Collection($Value) {\n if ($null -eq $Value) {\n return @()\n }\n return @($Value)\n}\n\nfunction Convert-TokenToString($Token) {\n if ($Token -is [Security.SecureString]) {\n $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token)\n try {\n return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)\n }\n finally {\n [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)\n }\n }\n return [string]$Token\n}\n\nfunction Get-ArmAccessTokenString() {\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://management.azure.com/').Token\n}\n\nfunction Get-SreAccessTokenString() {\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://azuresre.dev').Token\n}\n\n$subscriptionId = Get-RequiredEnv 'subscriptionId'\n$resourceGroupName = Get-RequiredEnv 'resourceGroupName'\n$agentName = Get-RequiredEnv 'agentName'\n$agentEndpoint = (Get-RequiredEnv 'agentEndpoint').TrimEnd('/')\n$recipePackageUri = Get-RequiredEnv 'recipePackageUri'\n$kustoConnectorUri = Get-OptionalEnv 'kustoConnectorUri'\n$armApiVersion = '2025-05-01-preview'\n$armBase = \"https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.App/agents/$agentName\"\n\nWrite-Output 'Connecting with deployment script managed identity...'\nConnect-AzAccount -Identity | Out-Null\nSet-AzContext -Subscription $subscriptionId | Out-Null\n\n$workRoot = Join-Path $env:TEMP 'sre-agent-recipe'\n$zipPath = Join-Path $env:TEMP 'sre-agent-recipe.zip'\nRemove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue\nNew-Item -Path $workRoot -ItemType Directory -Force | Out-Null\n\nWrite-Output \"Downloading SRE Agent recipe package: $recipePackageUri\"\nInvoke-WithRetry -Label 'download recipe package' -Action {\n Invoke-WebRequest -Uri $recipePackageUri -OutFile $zipPath\n} | Out-Null\nExpand-Archive -Path $zipPath -DestinationPath $workRoot -Force\n\n$extrasPath = Join-Path $workRoot 'extras.json'\nif (-not (Test-Path $extrasPath)) {\n throw \"Recipe package did not contain extras.json\"\n}\n\n$extras = Get-Content $extrasPath -Raw | ConvertFrom-Json -Depth 100\nWrite-Output \"Loaded recipe package from $extrasPath\"\n\nif ([string]::IsNullOrWhiteSpace($kustoConnectorUri)) {\n $extras.connectors = @(\n Get-Collection $extras.connectors | Where-Object {\n $type = Get-PropertyValue $_.properties @('dataConnectorType', 'type')\n $type -ine 'Kusto'\n }\n )\n}\nelse {\n foreach ($connector in Get-Collection $extras.connectors) {\n $type = Get-PropertyValue $connector.properties @('dataConnectorType', 'type')\n if ($type -ieq 'Kusto') {\n $connector.properties.dataSource = $kustoConnectorUri\n if (-not $connector.properties.PSObject.Properties['identity']) {\n $connector.properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\n }\n }\n }\n}\n\nfunction Invoke-ArmPutConnector([string]$Name, $Body) {\n $uri = \"$armBase/connectors/$Name`?api-version=$armApiVersion\"\n Invoke-WithRetry -Label \"ARM PUT connector $Name\" -Action {\n Invoke-JsonRest -Method PUT -Uri $uri -Token (Get-ArmAccessTokenString) -Body $Body\n } | Out-Null\n Write-Output \" ARM PUT connectors/${Name}: ok\"\n}\n\nfunction Invoke-SreApi([string]$Method, [string]$Path, $Body = $null) {\n $uri = \"$agentEndpoint$Path\"\n return Invoke-WithRetry -Label \"$Method $Path\" -MaxAttempts 21 -DelaySeconds 30 -Action {\n Invoke-JsonRest -Method $Method -Uri $uri -Token (Get-SreAccessTokenString) -Body $Body\n }\n}\n\nfunction Invoke-ExtendedPut([string]$Kind, [string]$Name, [string]$Type, $Properties) {\n $encodedName = [Uri]::EscapeDataString($Name)\n $body = @{\n name = $Name\n type = $Type\n tags = @()\n properties = $Properties\n }\n Invoke-SreApi -Method PUT -Path \"/api/v2/extendedAgent/$Kind/$encodedName\" -Body $body | Out-Null\n Write-Output \" PUT $Kind/${Name}: ok\"\n}\n\nWrite-Output 'Step 1/7: Applying connectors...'\nforeach ($connector in Get-Collection $extras.connectors) {\n $name = [string]$connector.name\n $properties = $connector.properties\n if (-not $properties.PSObject.Properties['identity']) {\n $properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\n }\n Invoke-ArmPutConnector -Name $name -Body @{ properties = $properties }\n}\n\nWrite-Output 'Step 2/7: Configuring built-in tools...'\n$overrides = Get-Collection $extras.builtInTools.overrides\nif ($overrides.Count -gt 0) {\n $body = @{\n overrides = @($overrides | ForEach-Object {\n @{\n name = $_.name\n enabled = [bool]$_.enabled\n }\n })\n }\n Invoke-SreApi -Method POST -Path '/api/v2/agent/tools/configure' -Body $body | Out-Null\n Write-Output \" built-in tools configured: $($overrides.Count) overrides\"\n}\n\nWrite-Output 'Step 3/7: Uploading knowledge sources...'\nforeach ($item in Get-Collection $extras.knowledgeItems) {\n $sourceName = Get-KnowledgeSourceName ([string]$item.name)\n $bytes = [Text.Encoding]::UTF8.GetBytes([string]$item.content)\n $body = @{\n properties = @{\n dataConnectorType = 'KnowledgeFile'\n dataSource = $sourceName\n extendedProperties = @{\n displayName = [string]$item.name\n fileName = [string]$item.name\n fileContent = [Convert]::ToBase64String($bytes)\n contentType = [string](Get-PropertyValue $item @('contentType') 'application/octet-stream')\n }\n }\n }\n Invoke-ArmPutConnector -Name $sourceName -Body $body\n Start-Sleep -Seconds 5\n}\n\nWrite-Output 'Step 4/7: Applying tools...'\nforeach ($tool in Get-Collection $extras.tools) {\n $name = [string]$tool.metadata.name\n $properties = $tool.spec\n if ($properties.type -eq 'PythonTool') {\n $properties.type = 'PythonFunctionTool'\n }\n Invoke-ExtendedPut -Kind 'tools' -Name $name -Type 'ExtendedAgentTool' -Properties $properties\n}\n\nWrite-Output 'Step 5/7: Applying skills...'\nforeach ($skill in Get-Collection $extras.skills) {\n $name = [string]$skill.metadata.name\n $tools = @()\n if ($skill.metadata.spec -and $skill.metadata.spec.tools) {\n $tools = Get-Collection $skill.metadata.spec.tools\n }\n $properties = @{\n name = $name\n description = [string](Get-PropertyValue $skill.metadata @('description'))\n tools = $tools\n skillContent = [string](Get-PropertyValue $skill @('skillContent'))\n additionalFiles = @()\n }\n Invoke-ExtendedPut -Kind 'skills' -Name $name -Type 'Skill' -Properties $properties\n}\n\nWrite-Output 'Step 6/7: Applying subagents...'\nforeach ($subagent in Get-Collection $extras.subagents) {\n $name = [string]$subagent.metadata.name\n Invoke-ExtendedPut -Kind 'agents' -Name $name -Type 'ExtendedAgent' -Properties $subagent.spec\n}\n\nWrite-Output 'Step 7/7: Applying scheduled tasks...'\n$scheduledTasks = Get-Collection $extras.scheduledTasks\nif ($scheduledTasks.Count -gt 0) {\n $existing = Invoke-SreApi -Method GET -Path '/api/v1/scheduledtasks'\n foreach ($task in $scheduledTasks) {\n $name = [string]$task.metadata.name\n foreach ($match in (Get-Collection $existing | Where-Object { $_.name -eq $name })) {\n if ($match.id) {\n Invoke-SreApi -Method DELETE -Path \"/api/v1/scheduledtasks/$($match.id)\" | Out-Null\n Write-Output \" deleted existing scheduledtasks/$name\"\n }\n }\n }\n\n foreach ($task in $scheduledTasks) {\n $name = [string]$task.metadata.name\n $spec = $task.spec\n $properties = @{\n name = [string](Get-PropertyValue $spec @('name') $name)\n description = [string](Get-PropertyValue $spec @('description'))\n cronExpression = [string](Get-PropertyValue $spec @('schedule', 'cronExpression', 'cron_expression'))\n agentPrompt = [string](Get-PropertyValue $spec @('prompt', 'agentPrompt', 'agent_prompt'))\n agentMode = [string](Get-PropertyValue $spec @('mode', 'agentMode', 'agent_mode') 'Review')\n isEnabled = [bool](Get-PropertyValue $spec @('enabled') $true)\n agent = [string](Get-PropertyValue $spec @('agent'))\n }\n Invoke-ExtendedPut -Kind 'scheduledtasks' -Name $name -Type 'ScheduledTask' -Properties $properties\n }\n}\n\nWrite-Output 'SRE Agent extras complete.'\n",
+ "identityName": "[format('id-sre-apply-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]",
+ "scriptName": "[format('apply-sre-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]",
+ "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "apiVersion": "2023-01-31",
+ "name": "[variables('identityName')]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]",
+ "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId'))]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]",
+ "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').principalId]",
+ "principalType": "ServicePrincipal"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deploymentScripts",
+ "apiVersion": "2023-08-01",
+ "name": "[variables('scriptName')]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]",
+ "kind": "AzurePowerShell",
+ "identity": {
+ "type": "UserAssigned",
+ "userAssignedIdentities": {
+ "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')))]": {}
+ }
+ },
+ "properties": {
+ "azPowerShellVersion": "11.0",
+ "retentionInterval": "PT1H",
+ "cleanupPreference": "OnSuccess",
+ "timeout": "PT2H",
+ "forceUpdateTag": "[parameters('forceUpdateTag')]",
+ "scriptContent": "[variables('$fxv#0')]",
+ "environmentVariables": [
+ {
+ "name": "subscriptionId",
+ "value": "[parameters('subscriptionId')]"
+ },
+ {
+ "name": "resourceGroupName",
+ "value": "[resourceGroup().name]"
+ },
+ {
+ "name": "agentName",
+ "value": "[parameters('agentName')]"
+ },
+ {
+ "name": "agentEndpoint",
+ "value": "[parameters('agentEndpoint')]"
+ },
+ {
+ "name": "recipePackageUri",
+ "value": "[parameters('recipePackageUri')]"
+ },
+ {
+ "name": "kustoConnectorUri",
+ "value": "[parameters('kustoConnectorUri')]"
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]",
+ "[extensionResourceId(resourceId('Microsoft.App/agents', parameters('agentName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId')))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "identityName": {
+ "type": "string",
+ "value": "[variables('identityName')]"
+ },
+ "scriptName": {
+ "type": "string",
+ "value": "[variables('scriptName')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]",
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]",
+ "targetRbac"
+ ]
+ }
+ ],
+ "outputs": {
+ "AZURE_RESOURCE_GROUP": {
+ "type": "string",
+ "value": "[parameters('resourceGroupName')]"
+ },
+ "AZURE_LOCATION": {
+ "type": "string",
+ "value": "[parameters('location')]"
+ },
+ "SRE_AGENT_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentName.value]"
+ },
+ "SRE_AGENT_ENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]"
+ },
+ "AGENT_PORTAL_URL": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPortalUrl.value]"
+ },
+ "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "SYSTEM_MANAGED_IDENTITY_TENANT_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentTenantId.value]"
+ },
+ "LOG_ANALYTICS_WORKSPACE_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]"
+ },
+ "APPLY_EXTRAS_SCRIPT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'apply-extras'), '2025-04-01').outputs.scriptName.value]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/deploy/sre-agent/14.0/createUiDefinition.json b/docs/deploy/sre-agent/14.0/createUiDefinition.json
new file mode 100644
index 000000000..87e82bcaa
--- /dev/null
+++ b/docs/deploy/sre-agent/14.0/createUiDefinition.json
@@ -0,0 +1,169 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#",
+ "handler": "Microsoft.Azure.CreateUIDef",
+ "version": "0.1.2-preview",
+ "parameters": {
+ "config": {
+ "basics": {
+ "description": "Deploy Azure SRE Agent with the FinOps toolkit recipe for FinOps hub analysis, capacity monitoring, scheduled tasks, and specialist agents.",
+ "location": {
+ "label": "Location",
+ "resourceTypes": [
+ "Microsoft.App/agents",
+ "Microsoft.Insights/components",
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "Microsoft.OperationalInsights/workspaces",
+ "Microsoft.Resources/deploymentScripts"
+ ]
+ }
+ }
+ },
+ "resourceTypes": [
+ "Microsoft.App/agents",
+ "Microsoft.Insights/components",
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "Microsoft.OperationalInsights/workspaces",
+ "Microsoft.Resources/deploymentScripts"
+ ],
+ "basics": [
+ {
+ "name": "resourceGroupName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent resource group",
+ "defaultValue": "finops-hub-sre",
+ "toolTip": "Resource group that will contain the Azure SRE Agent, monitoring resources, and deployment script identity.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-zA-Z0-9._()\\-]{1,90}$",
+ "validationMessage": "Enter a valid Azure resource group name."
+ },
+ "visible": true
+ },
+ {
+ "name": "agentName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent name",
+ "defaultValue": "finops-hub-sre",
+ "toolTip": "Name of the Azure SRE Agent resource.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,61}[a-zA-Z0-9]$",
+ "validationMessage": "Name must be 3-63 characters and can contain letters, numbers, and hyphens. The first and last characters must be alphanumeric."
+ },
+ "visible": true
+ }
+ ],
+ "steps": [
+ {
+ "name": "configuration",
+ "label": "Configuration",
+ "elements": [
+ {
+ "name": "finopsHubKustoConnectorUri",
+ "type": "Microsoft.Common.TextBox",
+ "label": "FinOps hub Kusto URI",
+ "defaultValue": "",
+ "toolTip": "Optional database-qualified Azure Data Explorer URI for the FinOps hub. Example: https://cluster.region.kusto.windows.net/Hub",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^https://.+\\.kusto\\.windows\\.net/.+",
+ "validationMessage": "Enter a database-qualified Kusto URI such as https://cluster.region.kusto.windows.net/Hub, or leave blank."
+ },
+ "visible": true
+ },
+ {
+ "name": "finopsHubKustoClusterResourceId",
+ "type": "Microsoft.Common.TextBox",
+ "label": "FinOps hub Kusto cluster resource ID",
+ "defaultValue": "",
+ "toolTip": "Optional Azure Data Explorer cluster resource ID. Provide this to assign the agent managed identity AllDatabasesViewer on the cluster.",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft\\.Kusto/clusters/.+",
+ "validationMessage": "Enter a valid Microsoft.Kusto/clusters resource ID, or leave blank."
+ },
+ "visible": true
+ },
+ {
+ "name": "targetResourceGroups",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Additional target resource groups",
+ "defaultValue": "",
+ "toolTip": "Optional comma-separated resource group names the agent can observe or act on. The agent resource group is always included.",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^[A-Za-z0-9_.()\\-]+(\\s*,\\s*[A-Za-z0-9_.()\\-]+)*$",
+ "validationMessage": "Enter a comma-separated list of resource group names without blank entries or trailing commas."
+ },
+ "visible": true
+ },
+ {
+ "name": "accessLevel",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Access level",
+ "defaultValue": "High",
+ "toolTip": "High adds Contributor on target resource groups for autonomous remediation workflows. Low grants read-only monitoring roles.",
+ "constraints": {
+ "allowedValues": [
+ {
+ "label": "High",
+ "value": "High"
+ },
+ {
+ "label": "Low",
+ "value": "Low"
+ }
+ ],
+ "required": true
+ },
+ "visible": true
+ },
+ {
+ "name": "actionMode",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Action mode",
+ "defaultValue": "autonomous",
+ "toolTip": "Review requires approval before write actions. Autonomous lets the agent run enabled actions within its assigned access.",
+ "constraints": {
+ "allowedValues": [
+ {
+ "label": "Autonomous",
+ "value": "autonomous"
+ },
+ {
+ "label": "Review",
+ "value": "review"
+ },
+ {
+ "label": "Read only",
+ "value": "readOnly"
+ }
+ ],
+ "required": true
+ },
+ "visible": true
+ },
+ {
+ "name": "enableSubscriptionReaderRole",
+ "type": "Microsoft.Common.CheckBox",
+ "label": "Grant subscription Reader to the agent",
+ "toolTip": "Recommended. Grants the agent managed identity Reader on the deployment subscription for inventory, Resource Graph, capacity, quota, and monitoring coverage workflows.",
+ "defaultValue": true,
+ "visible": true
+ }
+ ]
+ }
+ ],
+ "outputs": {
+ "resourceGroupName": "[basics('resourceGroupName')]",
+ "agentName": "[basics('agentName')]",
+ "location": "[location()]",
+ "targetResourceGroupNames": "[steps('configuration').targetResourceGroups]",
+ "finopsHubKustoConnectorUri": "[steps('configuration').finopsHubKustoConnectorUri]",
+ "finopsHubKustoClusterResourceId": "[steps('configuration').finopsHubKustoClusterResourceId]",
+ "accessLevel": "[steps('configuration').accessLevel]",
+ "actionMode": "[steps('configuration').actionMode]",
+ "enableSubscriptionReaderRole": "[steps('configuration').enableSubscriptionReaderRole]"
+ }
+ }
+}
diff --git a/docs/deploy/sre-agent/14.0/sre-agent-recipe.zip b/docs/deploy/sre-agent/14.0/sre-agent-recipe.zip
new file mode 100644
index 000000000..159ae7454
Binary files /dev/null and b/docs/deploy/sre-agent/14.0/sre-agent-recipe.zip differ
diff --git a/docs/deploy/sre-agent/latest/azuredeploy.json b/docs/deploy/sre-agent/latest/azuredeploy.json
new file mode 100644
index 000000000..4b6a6d0eb
--- /dev/null
+++ b/docs/deploy/sre-agent/latest/azuredeploy.json
@@ -0,0 +1,1119 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "2403415415579441400"
+ }
+ },
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "metadata": {
+ "description": "Resource group that holds the SRE Agent resources."
+ }
+ },
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "location": {
+ "type": "string",
+ "defaultValue": "eastus2",
+ "allowedValues": [
+ "australiaeast",
+ "canadacentral",
+ "eastus2",
+ "francecentral",
+ "koreacentral",
+ "swedencentral",
+ "uksouth"
+ ],
+ "metadata": {
+ "description": "Primary location for all resources."
+ }
+ },
+ "targetResourceGroups": {
+ "type": "array",
+ "defaultValue": [],
+ "metadata": {
+ "description": "Resource groups the agent can observe or act on. The agent resource group is always included."
+ }
+ },
+ "targetResourceGroupNames": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Comma-separated resource groups the agent can observe or act on. Used by the Azure portal form."
+ }
+ },
+ "finopsHubKustoConnectorUri": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional database-qualified FinOps Hub Kusto connector URI. Example: https://..kusto.windows.net/Hub"
+ }
+ },
+ "finopsHubKustoClusterResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "defaultValue": "High",
+ "allowedValues": [
+ "Low",
+ "High"
+ ],
+ "metadata": {
+ "description": "Agent access level."
+ }
+ },
+ "actionMode": {
+ "type": "string",
+ "defaultValue": "autonomous",
+ "allowedValues": [
+ "review",
+ "autonomous",
+ "readOnly"
+ ],
+ "metadata": {
+ "description": "Agent action mode."
+ }
+ },
+ "upgradeChannel": {
+ "type": "string",
+ "defaultValue": "Preview",
+ "allowedValues": [
+ "Stable",
+ "Preview"
+ ],
+ "metadata": {
+ "description": "Agent upgrade channel."
+ }
+ },
+ "defaultModelProvider": {
+ "type": "string",
+ "defaultValue": "MicrosoftFoundry",
+ "allowedValues": [
+ "MicrosoftFoundry",
+ "Anthropic"
+ ],
+ "metadata": {
+ "description": "Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal."
+ }
+ },
+ "defaultModelName": {
+ "type": "string",
+ "defaultValue": "Automatic",
+ "metadata": {
+ "description": "Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider."
+ }
+ },
+ "monthlyAgentUnitLimit": {
+ "type": "int",
+ "defaultValue": 10000,
+ "minValue": 1,
+ "metadata": {
+ "description": "Monthly agent unit limit."
+ }
+ },
+ "experimentalSettings": {
+ "type": "object",
+ "defaultValue": {
+ "EnableSandboxGroup": true,
+ "EnableWorkspaceTools": true
+ },
+ "metadata": {
+ "description": "Agent experimental settings."
+ }
+ },
+ "enableSubscriptionReaderRole": {
+ "type": "bool",
+ "defaultValue": true,
+ "metadata": {
+ "description": "Assign Reader on the deployment subscription to the agent managed identity."
+ }
+ },
+ "recipePackageUri": {
+ "type": "string",
+ "defaultValue": "[uri(deployment().properties.templateLink.uri, 'sre-agent-recipe.zip')]",
+ "metadata": {
+ "description": "Public URI for the generated SRE Agent recipe package. Deploy-to-Azure links derive this from the template URI."
+ }
+ },
+ "forceUpdateTag": {
+ "type": "string",
+ "defaultValue": "[utcNow()]",
+ "metadata": {
+ "description": "Forces the recipe deployment script to run when the template is redeployed."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {
+ "finops-toolkit": "sre-agent",
+ "source": "microsoft-finops-toolkit"
+ },
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "copy": [
+ {
+ "name": "targetRgIds",
+ "count": "[length(variables('targetRgs'))]",
+ "input": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex('targetRgIds')])]"
+ }
+ ],
+ "rawTargetResourceGroups": "[split(replace(parameters('targetResourceGroupNames'), ' ', ''), ',')]",
+ "parsedTargetResourceGroups": "[filter(variables('rawTargetResourceGroups'), lambda('rgName', not(empty(lambdaVariables('rgName')))))]",
+ "targetRgs": "[union(createArray(parameters('resourceGroupName')), parameters('targetResourceGroups'), variables('parsedTargetResourceGroups'))]",
+ "agentResourceGroupId": "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]",
+ "namingSeed": "[toLower(format('{0}|{1}|{2}', subscription().subscriptionId, variables('agentResourceGroupId'), parameters('agentName')))]",
+ "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
+ "hasKustoCluster": "[not(empty(parameters('finopsHubKustoClusterResourceId')))]",
+ "kustoClusterSubscriptionId": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[2], '')]",
+ "kustoClusterResourceGroupName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[4], '')]",
+ "kustoClusterName": "[if(variables('hasKustoCluster'), split(parameters('finopsHubKustoClusterResourceId'), '/')[8], '')]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "apiVersion": "2024-03-01",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "condition": "[parameters('enableSubscriptionReaderRole')]",
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "name": "[guid(subscription().id, variables('namingSeed'), variables('readerRoleId'))]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('readerRoleId'))]",
+ "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]",
+ "principalType": "ServicePrincipal"
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "resources-deployment",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "agentName": {
+ "value": "[parameters('agentName')]"
+ },
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "namingSeed": {
+ "value": "[variables('namingSeed')]"
+ },
+ "targetResourceGroupIds": {
+ "value": "[variables('targetRgIds')]"
+ },
+ "accessLevel": {
+ "value": "[parameters('accessLevel')]"
+ },
+ "actionMode": {
+ "value": "[parameters('actionMode')]"
+ },
+ "upgradeChannel": {
+ "value": "[parameters('upgradeChannel')]"
+ },
+ "defaultModelProvider": {
+ "value": "[parameters('defaultModelProvider')]"
+ },
+ "defaultModelName": {
+ "value": "[parameters('defaultModelName')]"
+ },
+ "monthlyAgentUnitLimit": {
+ "value": "[parameters('monthlyAgentUnitLimit')]"
+ },
+ "experimentalSettings": {
+ "value": "[parameters('experimentalSettings')]"
+ },
+ "tags": {
+ "value": "[parameters('tags')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "11748842726639098374"
+ }
+ },
+ "parameters": {
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for all resources."
+ }
+ },
+ "namingSeed": {
+ "type": "string",
+ "metadata": {
+ "description": "Deterministic naming seed built from subscription ID, agent resource group ID, and agent name."
+ }
+ },
+ "targetResourceGroupIds": {
+ "type": "array",
+ "metadata": {
+ "description": "Resource group IDs shown as managed resources in the SRE Agent."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent access level."
+ }
+ },
+ "actionMode": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent action mode."
+ }
+ },
+ "upgradeChannel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent upgrade channel."
+ }
+ },
+ "defaultModelProvider": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model provider."
+ }
+ },
+ "defaultModelName": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model name."
+ }
+ },
+ "monthlyAgentUnitLimit": {
+ "type": "int",
+ "metadata": {
+ "description": "Monthly agent unit limit."
+ }
+ },
+ "experimentalSettings": {
+ "type": "object",
+ "metadata": {
+ "description": "Agent experimental settings."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "uniqueSuffix": "[uniqueString(parameters('namingSeed'))]",
+ "logAnalyticsName": "[format('law-{0}', variables('uniqueSuffix'))]",
+ "appInsightsName": "[format('appi-{0}', variables('uniqueSuffix'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "monitoring",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "logAnalyticsName": {
+ "value": "[variables('logAnalyticsName')]"
+ },
+ "appInsightsName": {
+ "value": "[variables('appInsightsName')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "9164683275614055037"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for resources"
+ }
+ },
+ "logAnalyticsName": {
+ "type": "string",
+ "metadata": {
+ "description": "Log Analytics Workspace name"
+ }
+ },
+ "appInsightsName": {
+ "type": "string",
+ "metadata": {
+ "description": "Application Insights name"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.OperationalInsights/workspaces",
+ "apiVersion": "2023-09-01",
+ "name": "[parameters('logAnalyticsName')]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "sku": {
+ "name": "PerGB2018"
+ },
+ "retentionInDays": 30
+ }
+ },
+ {
+ "type": "Microsoft.Insights/components",
+ "apiVersion": "2020-02-02",
+ "name": "[parameters('appInsightsName')]",
+ "location": "[parameters('location')]",
+ "kind": "web",
+ "properties": {
+ "Application_Type": "web",
+ "Request_Source": "IbizaAIExtension",
+ "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "logAnalyticsWorkspaceId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsName'))]"
+ },
+ "logAnalyticsWorkspaceName": {
+ "type": "string",
+ "value": "[parameters('logAnalyticsName')]"
+ },
+ "appInsightsId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]"
+ },
+ "appInsightsAppId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').AppId]"
+ },
+ "appInsightsConnectionString": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2020-02-02').ConnectionString]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "sre-agent",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "agentName": {
+ "value": "[parameters('agentName')]"
+ },
+ "appInsightsAppId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsAppId.value]"
+ },
+ "appInsightsConnectionString": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]"
+ },
+ "appInsightsId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsId.value]"
+ },
+ "managedResourceGroupIds": {
+ "value": "[parameters('targetResourceGroupIds')]"
+ },
+ "accessLevel": {
+ "value": "[parameters('accessLevel')]"
+ },
+ "actionMode": {
+ "value": "[parameters('actionMode')]"
+ },
+ "upgradeChannel": {
+ "value": "[parameters('upgradeChannel')]"
+ },
+ "defaultModelProvider": {
+ "value": "[parameters('defaultModelProvider')]"
+ },
+ "defaultModelName": {
+ "value": "[parameters('defaultModelName')]"
+ },
+ "monthlyAgentUnitLimit": {
+ "value": "[parameters('monthlyAgentUnitLimit')]"
+ },
+ "experimentalSettings": {
+ "value": "[parameters('experimentalSettings')]"
+ },
+ "tags": {
+ "value": "[parameters('tags')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "6562535881153200016"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for resources."
+ }
+ },
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "appInsightsAppId": {
+ "type": "string",
+ "metadata": {
+ "description": "Application Insights App ID."
+ }
+ },
+ "appInsightsConnectionString": {
+ "type": "securestring",
+ "metadata": {
+ "description": "Application Insights connection string."
+ }
+ },
+ "appInsightsId": {
+ "type": "string",
+ "metadata": {
+ "description": "Application Insights resource ID."
+ }
+ },
+ "managedResourceGroupIds": {
+ "type": "array",
+ "metadata": {
+ "description": "Resource group IDs to add as managed resources."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent access level."
+ }
+ },
+ "actionMode": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent action mode."
+ }
+ },
+ "upgradeChannel": {
+ "type": "string",
+ "metadata": {
+ "description": "Agent upgrade channel."
+ }
+ },
+ "defaultModelProvider": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model provider."
+ }
+ },
+ "defaultModelName": {
+ "type": "string",
+ "metadata": {
+ "description": "Default SRE Agent model name."
+ }
+ },
+ "monthlyAgentUnitLimit": {
+ "type": "int",
+ "metadata": {
+ "description": "Monthly agent unit limit."
+ }
+ },
+ "experimentalSettings": {
+ "type": "object",
+ "metadata": {
+ "description": "Agent experimental settings."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.App/agents",
+ "apiVersion": "2026-01-01",
+ "name": "[parameters('agentName')]",
+ "location": "[parameters('location')]",
+ "tags": "[union(parameters('tags'), createObject('hidden-link: /app-insights-resource-id', parameters('appInsightsId'), 'source', 'microsoft-sre-agent-starter-lab', 'finops-toolkit', 'sre-agent'))]",
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "properties": {
+ "knowledgeGraphConfiguration": {
+ "managedResources": "[parameters('managedResourceGroupIds')]",
+ "identity": "system"
+ },
+ "actionConfiguration": {
+ "mode": "[parameters('actionMode')]",
+ "identity": "system",
+ "accessLevel": "[parameters('accessLevel')]"
+ },
+ "upgradeChannel": "[parameters('upgradeChannel')]",
+ "defaultModel": {
+ "provider": "[parameters('defaultModelProvider')]",
+ "name": "[parameters('defaultModelName')]"
+ },
+ "monthlyAgentUnitLimit": "[parameters('monthlyAgentUnitLimit')]",
+ "experimentalSettings": "[parameters('experimentalSettings')]",
+ "mcpServers": [],
+ "logConfiguration": {
+ "applicationInsightsConfiguration": {
+ "appId": "[parameters('appInsightsAppId')]",
+ "connectionString": "[parameters('appInsightsConnectionString')]"
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]",
+ "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), deployer().objectId, variables('sreAgentAdminRoleId'))]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]",
+ "principalId": "[deployer().objectId]",
+ "principalType": "User"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.App/agents', parameters('agentName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "agentName": {
+ "type": "string",
+ "value": "[parameters('agentName')]"
+ },
+ "agentId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.App/agents', parameters('agentName'))]"
+ },
+ "agentEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01').agentEndpoint]"
+ },
+ "agentPortalUrl": {
+ "type": "string",
+ "value": "[format('https://sre.azure.com/#/agent/{0}/{1}/{2}', subscription().subscriptionId, resourceGroup().name, parameters('agentName'))]"
+ },
+ "agentPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.principalId]"
+ },
+ "agentTenantId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.App/agents', parameters('agentName')), '2026-01-01', 'full').identity.tenantId]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', 'monitoring')]"
+ ]
+ }
+ ],
+ "outputs": {
+ "agentName": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentName.value]"
+ },
+ "agentEndpoint": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentEndpoint.value]"
+ },
+ "agentPortalUrl": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPortalUrl.value]"
+ },
+ "agentPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "agentTenantId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'sre-agent'), '2025-04-01').outputs.agentTenantId.value]"
+ },
+ "logAnalyticsWorkspaceId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]"
+ ]
+ },
+ {
+ "copy": {
+ "name": "targetRbac",
+ "count": "[length(variables('targetRgs'))]"
+ },
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('target-rbac-{0}', uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('targetRgs')[copyIndex()])), variables('namingSeed')))]",
+ "resourceGroup": "[variables('targetRgs')[copyIndex()]]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "principalId": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "accessLevel": {
+ "value": "[parameters('accessLevel')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "6305641259472431615"
+ }
+ },
+ "parameters": {
+ "principalId": {
+ "type": "string",
+ "metadata": {
+ "description": "Principal ID of the managed identity to assign roles to."
+ }
+ },
+ "accessLevel": {
+ "type": "string",
+ "allowedValues": [
+ "Low",
+ "High"
+ ],
+ "metadata": {
+ "description": "Agent access level."
+ }
+ }
+ },
+ "variables": {
+ "readerRoleId": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
+ "monitoringReaderRoleId": "43d0d8ad-25c7-4714-9337-8ba259a9fe05",
+ "logAnalyticsReaderRoleId": "73c42c96-874c-492b-b04d-ab87d138a893",
+ "contributorRoleId": "b24988ac-6180-42a0-ab88-20f7382dd24c",
+ "roleIds": "[if(equals(parameters('accessLevel'), 'High'), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId'), variables('contributorRoleId')), createArray(variables('readerRoleId'), variables('monitoringReaderRoleId'), variables('logAnalyticsReaderRoleId')))]"
+ },
+ "resources": [
+ {
+ "copy": {
+ "name": "roleAssignments",
+ "count": "[length(variables('roleIds'))]"
+ },
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "name": "[guid(resourceGroup().id, parameters('principalId'), variables('roleIds')[copyIndex()])]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('roleIds')[copyIndex()])]",
+ "principalId": "[parameters('principalId')]",
+ "principalType": "ServicePrincipal"
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]"
+ ]
+ },
+ {
+ "condition": "[variables('hasKustoCluster')]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('kusto-rbac-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed')))]",
+ "subscriptionId": "[variables('kustoClusterSubscriptionId')]",
+ "resourceGroup": "[variables('kustoClusterResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "clusterName": {
+ "value": "[variables('kustoClusterName')]"
+ },
+ "principalApplicationId": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "principalTenantId": {
+ "value": "[tenant().tenantId]"
+ },
+ "principalAssignmentName": {
+ "value": "[format('sre-agent-{0}', uniqueString(parameters('finopsHubKustoClusterResourceId'), variables('namingSeed'), 'all-db-viewer'))]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "15341408879505554256"
+ }
+ },
+ "parameters": {
+ "clusterName": {
+ "type": "string",
+ "metadata": {
+ "description": "Kusto cluster name."
+ }
+ },
+ "principalApplicationId": {
+ "type": "string",
+ "metadata": {
+ "description": "Microsoft Entra application/client ID to assign to the Kusto cluster."
+ }
+ },
+ "principalTenantId": {
+ "type": "string",
+ "metadata": {
+ "description": "Principal tenant ID."
+ }
+ },
+ "principalAssignmentName": {
+ "type": "string",
+ "metadata": {
+ "description": "Stable principal assignment name."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Kusto/clusters/principalAssignments",
+ "apiVersion": "2024-04-13",
+ "name": "[format('{0}/{1}', parameters('clusterName'), parameters('principalAssignmentName'))]",
+ "properties": {
+ "principalId": "[parameters('principalApplicationId')]",
+ "principalType": "App",
+ "role": "AllDatabasesViewer",
+ "tenantId": "[parameters('principalTenantId')]"
+ }
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "apply-extras",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "agentName": {
+ "value": "[parameters('agentName')]"
+ },
+ "agentEndpoint": {
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]"
+ },
+ "subscriptionId": {
+ "value": "[subscription().subscriptionId]"
+ },
+ "recipePackageUri": {
+ "value": "[parameters('recipePackageUri')]"
+ },
+ "kustoConnectorUri": {
+ "value": "[parameters('finopsHubKustoConnectorUri')]"
+ },
+ "forceUpdateTag": {
+ "value": "[parameters('forceUpdateTag')]"
+ },
+ "tags": {
+ "value": "[parameters('tags')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "5168893811325309322"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Location for the deployment script resources."
+ }
+ },
+ "agentName": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent name."
+ }
+ },
+ "agentEndpoint": {
+ "type": "string",
+ "metadata": {
+ "description": "SRE Agent data-plane endpoint."
+ }
+ },
+ "subscriptionId": {
+ "type": "string",
+ "metadata": {
+ "description": "Subscription that contains the SRE Agent."
+ }
+ },
+ "recipePackageUri": {
+ "type": "string",
+ "metadata": {
+ "description": "Public URI for the generated SRE Agent recipe package."
+ }
+ },
+ "kustoConnectorUri": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional database-qualified Kusto connector URI."
+ }
+ },
+ "forceUpdateTag": {
+ "type": "string",
+ "metadata": {
+ "description": "Forces the deployment script to run when the template is redeployed."
+ }
+ },
+ "tags": {
+ "type": "object",
+ "defaultValue": {},
+ "metadata": {
+ "description": "Azure resource tags."
+ }
+ }
+ },
+ "variables": {
+ "$fxv#0": "# Copyright (c) Microsoft Corporation.\n# Licensed under the MIT License.\n\n$ErrorActionPreference = 'Stop'\n\nfunction Get-RequiredEnv($Name) {\n $value = [Environment]::GetEnvironmentVariable($Name)\n if ([string]::IsNullOrWhiteSpace($value)) {\n throw \"Missing required environment variable: $Name\"\n }\n return $value\n}\n\nfunction Get-OptionalEnv($Name) {\n return [Environment]::GetEnvironmentVariable($Name)\n}\n\nfunction ConvertTo-BodyJson($Value) {\n return ($Value | ConvertTo-Json -Depth 100 -Compress)\n}\n\nfunction Get-PropertyValue($Object, [string[]]$Names, $Default = '') {\n foreach ($name in $Names) {\n if ($null -ne $Object -and $Object.PSObject.Properties[$name] -and $null -ne $Object.$name) {\n return $Object.$name\n }\n }\n return $Default\n}\n\nfunction Get-HttpStatusCode($Exception) {\n if ($Exception.Response -and $Exception.Response.StatusCode) {\n return [int]$Exception.Response.StatusCode\n }\n return $null\n}\n\nfunction Invoke-WithRetry([scriptblock]$Action, [string]$Label, [int]$MaxAttempts = 5, [int]$DelaySeconds = 15) {\n for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {\n try {\n return & $Action\n }\n catch {\n $statusCode = Get-HttpStatusCode $_.Exception\n if ($statusCode -and $statusCode -notin @(401, 403, 408, 429, 500, 502, 503, 504)) {\n throw\n }\n if ($attempt -eq $MaxAttempts) {\n throw\n }\n Write-Output \"$Label attempt $attempt/$MaxAttempts failed: $($_.Exception.Message)\"\n Start-Sleep -Seconds $DelaySeconds\n }\n }\n}\n\nfunction Invoke-JsonRest([string]$Method, [string]$Uri, [string]$Token, $Body = $null) {\n $headers = @{\n Authorization = \"Bearer $Token\"\n 'Content-Type' = 'application/json'\n }\n $parameters = @{\n Method = $Method\n Uri = $Uri\n Headers = $headers\n }\n if ($null -ne $Body) {\n $parameters.Body = ConvertTo-BodyJson $Body\n }\n return Invoke-RestMethod @parameters\n}\n\nfunction Get-KnowledgeSourceName([string]$FileName) {\n $name = [IO.Path]::GetFileName($FileName).ToLowerInvariant() -replace '[^a-z0-9-]+', '-'\n $name = $name -replace '-+', '-'\n return $name.Trim('-')\n}\n\nfunction Get-Collection($Value) {\n if ($null -eq $Value) {\n return @()\n }\n return @($Value)\n}\n\nfunction Convert-TokenToString($Token) {\n if ($Token -is [Security.SecureString]) {\n $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token)\n try {\n return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)\n }\n finally {\n [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)\n }\n }\n return [string]$Token\n}\n\nfunction Get-ArmAccessTokenString() {\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://management.azure.com/').Token\n}\n\nfunction Get-SreAccessTokenString() {\n return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://azuresre.dev').Token\n}\n\n$subscriptionId = Get-RequiredEnv 'subscriptionId'\n$resourceGroupName = Get-RequiredEnv 'resourceGroupName'\n$agentName = Get-RequiredEnv 'agentName'\n$agentEndpoint = (Get-RequiredEnv 'agentEndpoint').TrimEnd('/')\n$recipePackageUri = Get-RequiredEnv 'recipePackageUri'\n$kustoConnectorUri = Get-OptionalEnv 'kustoConnectorUri'\n$armApiVersion = '2025-05-01-preview'\n$armBase = \"https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.App/agents/$agentName\"\n\nWrite-Output 'Connecting with deployment script managed identity...'\nConnect-AzAccount -Identity | Out-Null\nSet-AzContext -Subscription $subscriptionId | Out-Null\n\n$workRoot = Join-Path $env:TEMP 'sre-agent-recipe'\n$zipPath = Join-Path $env:TEMP 'sre-agent-recipe.zip'\nRemove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue\nNew-Item -Path $workRoot -ItemType Directory -Force | Out-Null\n\nWrite-Output \"Downloading SRE Agent recipe package: $recipePackageUri\"\nInvoke-WithRetry -Label 'download recipe package' -Action {\n Invoke-WebRequest -Uri $recipePackageUri -OutFile $zipPath\n} | Out-Null\nExpand-Archive -Path $zipPath -DestinationPath $workRoot -Force\n\n$extrasPath = Join-Path $workRoot 'extras.json'\nif (-not (Test-Path $extrasPath)) {\n throw \"Recipe package did not contain extras.json\"\n}\n\n$extras = Get-Content $extrasPath -Raw | ConvertFrom-Json -Depth 100\nWrite-Output \"Loaded recipe package from $extrasPath\"\n\nif ([string]::IsNullOrWhiteSpace($kustoConnectorUri)) {\n $extras.connectors = @(\n Get-Collection $extras.connectors | Where-Object {\n $type = Get-PropertyValue $_.properties @('dataConnectorType', 'type')\n $type -ine 'Kusto'\n }\n )\n}\nelse {\n foreach ($connector in Get-Collection $extras.connectors) {\n $type = Get-PropertyValue $connector.properties @('dataConnectorType', 'type')\n if ($type -ieq 'Kusto') {\n $connector.properties.dataSource = $kustoConnectorUri\n if (-not $connector.properties.PSObject.Properties['identity']) {\n $connector.properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\n }\n }\n }\n}\n\nfunction Invoke-ArmPutConnector([string]$Name, $Body) {\n $uri = \"$armBase/connectors/$Name`?api-version=$armApiVersion\"\n Invoke-WithRetry -Label \"ARM PUT connector $Name\" -Action {\n Invoke-JsonRest -Method PUT -Uri $uri -Token (Get-ArmAccessTokenString) -Body $Body\n } | Out-Null\n Write-Output \" ARM PUT connectors/${Name}: ok\"\n}\n\nfunction Invoke-SreApi([string]$Method, [string]$Path, $Body = $null) {\n $uri = \"$agentEndpoint$Path\"\n return Invoke-WithRetry -Label \"$Method $Path\" -MaxAttempts 21 -DelaySeconds 30 -Action {\n Invoke-JsonRest -Method $Method -Uri $uri -Token (Get-SreAccessTokenString) -Body $Body\n }\n}\n\nfunction Invoke-ExtendedPut([string]$Kind, [string]$Name, [string]$Type, $Properties) {\n $encodedName = [Uri]::EscapeDataString($Name)\n $body = @{\n name = $Name\n type = $Type\n tags = @()\n properties = $Properties\n }\n Invoke-SreApi -Method PUT -Path \"/api/v2/extendedAgent/$Kind/$encodedName\" -Body $body | Out-Null\n Write-Output \" PUT $Kind/${Name}: ok\"\n}\n\nWrite-Output 'Step 1/7: Applying connectors...'\nforeach ($connector in Get-Collection $extras.connectors) {\n $name = [string]$connector.name\n $properties = $connector.properties\n if (-not $properties.PSObject.Properties['identity']) {\n $properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'\n }\n Invoke-ArmPutConnector -Name $name -Body @{ properties = $properties }\n}\n\nWrite-Output 'Step 2/7: Configuring built-in tools...'\n$overrides = Get-Collection $extras.builtInTools.overrides\nif ($overrides.Count -gt 0) {\n $body = @{\n overrides = @($overrides | ForEach-Object {\n @{\n name = $_.name\n enabled = [bool]$_.enabled\n }\n })\n }\n Invoke-SreApi -Method POST -Path '/api/v2/agent/tools/configure' -Body $body | Out-Null\n Write-Output \" built-in tools configured: $($overrides.Count) overrides\"\n}\n\nWrite-Output 'Step 3/7: Uploading knowledge sources...'\nforeach ($item in Get-Collection $extras.knowledgeItems) {\n $sourceName = Get-KnowledgeSourceName ([string]$item.name)\n $bytes = [Text.Encoding]::UTF8.GetBytes([string]$item.content)\n $body = @{\n properties = @{\n dataConnectorType = 'KnowledgeFile'\n dataSource = $sourceName\n extendedProperties = @{\n displayName = [string]$item.name\n fileName = [string]$item.name\n fileContent = [Convert]::ToBase64String($bytes)\n contentType = [string](Get-PropertyValue $item @('contentType') 'application/octet-stream')\n }\n }\n }\n Invoke-ArmPutConnector -Name $sourceName -Body $body\n Start-Sleep -Seconds 5\n}\n\nWrite-Output 'Step 4/7: Applying tools...'\nforeach ($tool in Get-Collection $extras.tools) {\n $name = [string]$tool.metadata.name\n $properties = $tool.spec\n if ($properties.type -eq 'PythonTool') {\n $properties.type = 'PythonFunctionTool'\n }\n Invoke-ExtendedPut -Kind 'tools' -Name $name -Type 'ExtendedAgentTool' -Properties $properties\n}\n\nWrite-Output 'Step 5/7: Applying skills...'\nforeach ($skill in Get-Collection $extras.skills) {\n $name = [string]$skill.metadata.name\n $tools = @()\n if ($skill.metadata.spec -and $skill.metadata.spec.tools) {\n $tools = Get-Collection $skill.metadata.spec.tools\n }\n $properties = @{\n name = $name\n description = [string](Get-PropertyValue $skill.metadata @('description'))\n tools = $tools\n skillContent = [string](Get-PropertyValue $skill @('skillContent'))\n additionalFiles = @()\n }\n Invoke-ExtendedPut -Kind 'skills' -Name $name -Type 'Skill' -Properties $properties\n}\n\nWrite-Output 'Step 6/7: Applying subagents...'\nforeach ($subagent in Get-Collection $extras.subagents) {\n $name = [string]$subagent.metadata.name\n Invoke-ExtendedPut -Kind 'agents' -Name $name -Type 'ExtendedAgent' -Properties $subagent.spec\n}\n\nWrite-Output 'Step 7/7: Applying scheduled tasks...'\n$scheduledTasks = Get-Collection $extras.scheduledTasks\nif ($scheduledTasks.Count -gt 0) {\n $existing = Invoke-SreApi -Method GET -Path '/api/v1/scheduledtasks'\n foreach ($task in $scheduledTasks) {\n $name = [string]$task.metadata.name\n foreach ($match in (Get-Collection $existing | Where-Object { $_.name -eq $name })) {\n if ($match.id) {\n Invoke-SreApi -Method DELETE -Path \"/api/v1/scheduledtasks/$($match.id)\" | Out-Null\n Write-Output \" deleted existing scheduledtasks/$name\"\n }\n }\n }\n\n foreach ($task in $scheduledTasks) {\n $name = [string]$task.metadata.name\n $spec = $task.spec\n $properties = @{\n name = [string](Get-PropertyValue $spec @('name') $name)\n description = [string](Get-PropertyValue $spec @('description'))\n cronExpression = [string](Get-PropertyValue $spec @('schedule', 'cronExpression', 'cron_expression'))\n agentPrompt = [string](Get-PropertyValue $spec @('prompt', 'agentPrompt', 'agent_prompt'))\n agentMode = [string](Get-PropertyValue $spec @('mode', 'agentMode', 'agent_mode') 'Review')\n isEnabled = [bool](Get-PropertyValue $spec @('enabled') $true)\n agent = [string](Get-PropertyValue $spec @('agent'))\n }\n Invoke-ExtendedPut -Kind 'scheduledtasks' -Name $name -Type 'ScheduledTask' -Properties $properties\n }\n}\n\nWrite-Output 'SRE Agent extras complete.'\n",
+ "identityName": "[format('id-sre-apply-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]",
+ "scriptName": "[format('apply-sre-{0}', uniqueString(resourceGroup().id, parameters('agentName')))]",
+ "sreAgentAdminRoleId": "e79298df-d852-4c6d-84f9-5d13249d1e55"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "apiVersion": "2023-01-31",
+ "name": "[variables('identityName')]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]"
+ },
+ {
+ "type": "Microsoft.Authorization/roleAssignments",
+ "apiVersion": "2022-04-01",
+ "scope": "[resourceId('Microsoft.App/agents', parameters('agentName'))]",
+ "name": "[guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId'))]",
+ "properties": {
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('sreAgentAdminRoleId'))]",
+ "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), '2023-01-31').principalId]",
+ "principalType": "ServicePrincipal"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deploymentScripts",
+ "apiVersion": "2023-08-01",
+ "name": "[variables('scriptName')]",
+ "location": "[parameters('location')]",
+ "tags": "[parameters('tags')]",
+ "kind": "AzurePowerShell",
+ "identity": {
+ "type": "UserAssigned",
+ "userAssignedIdentities": {
+ "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')))]": {}
+ }
+ },
+ "properties": {
+ "azPowerShellVersion": "11.0",
+ "retentionInterval": "PT1H",
+ "cleanupPreference": "OnSuccess",
+ "timeout": "PT2H",
+ "forceUpdateTag": "[parameters('forceUpdateTag')]",
+ "scriptContent": "[variables('$fxv#0')]",
+ "environmentVariables": [
+ {
+ "name": "subscriptionId",
+ "value": "[parameters('subscriptionId')]"
+ },
+ {
+ "name": "resourceGroupName",
+ "value": "[resourceGroup().name]"
+ },
+ {
+ "name": "agentName",
+ "value": "[parameters('agentName')]"
+ },
+ {
+ "name": "agentEndpoint",
+ "value": "[parameters('agentEndpoint')]"
+ },
+ {
+ "name": "recipePackageUri",
+ "value": "[parameters('recipePackageUri')]"
+ },
+ {
+ "name": "kustoConnectorUri",
+ "value": "[parameters('kustoConnectorUri')]"
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]",
+ "[extensionResourceId(resourceId('Microsoft.App/agents', parameters('agentName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.App/agents', parameters('agentName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName')), variables('sreAgentAdminRoleId')))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "identityName": {
+ "type": "string",
+ "value": "[variables('identityName')]"
+ },
+ "scriptName": {
+ "type": "string",
+ "value": "[variables('scriptName')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment')]",
+ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', parameters('resourceGroupName'))]",
+ "targetRbac"
+ ]
+ }
+ ],
+ "outputs": {
+ "AZURE_RESOURCE_GROUP": {
+ "type": "string",
+ "value": "[parameters('resourceGroupName')]"
+ },
+ "AZURE_LOCATION": {
+ "type": "string",
+ "value": "[parameters('location')]"
+ },
+ "SRE_AGENT_NAME": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentName.value]"
+ },
+ "SRE_AGENT_ENDPOINT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentEndpoint.value]"
+ },
+ "AGENT_PORTAL_URL": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPortalUrl.value]"
+ },
+ "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentPrincipalId.value]"
+ },
+ "SYSTEM_MANAGED_IDENTITY_TENANT_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.agentTenantId.value]"
+ },
+ "LOG_ANALYTICS_WORKSPACE_ID": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'resources-deployment'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]"
+ },
+ "APPLY_EXTRAS_SCRIPT": {
+ "type": "string",
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', 'apply-extras'), '2025-04-01').outputs.scriptName.value]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/deploy/sre-agent/latest/createUiDefinition.json b/docs/deploy/sre-agent/latest/createUiDefinition.json
new file mode 100644
index 000000000..87e82bcaa
--- /dev/null
+++ b/docs/deploy/sre-agent/latest/createUiDefinition.json
@@ -0,0 +1,169 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#",
+ "handler": "Microsoft.Azure.CreateUIDef",
+ "version": "0.1.2-preview",
+ "parameters": {
+ "config": {
+ "basics": {
+ "description": "Deploy Azure SRE Agent with the FinOps toolkit recipe for FinOps hub analysis, capacity monitoring, scheduled tasks, and specialist agents.",
+ "location": {
+ "label": "Location",
+ "resourceTypes": [
+ "Microsoft.App/agents",
+ "Microsoft.Insights/components",
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "Microsoft.OperationalInsights/workspaces",
+ "Microsoft.Resources/deploymentScripts"
+ ]
+ }
+ }
+ },
+ "resourceTypes": [
+ "Microsoft.App/agents",
+ "Microsoft.Insights/components",
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "Microsoft.OperationalInsights/workspaces",
+ "Microsoft.Resources/deploymentScripts"
+ ],
+ "basics": [
+ {
+ "name": "resourceGroupName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent resource group",
+ "defaultValue": "finops-hub-sre",
+ "toolTip": "Resource group that will contain the Azure SRE Agent, monitoring resources, and deployment script identity.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-zA-Z0-9._()\\-]{1,90}$",
+ "validationMessage": "Enter a valid Azure resource group name."
+ },
+ "visible": true
+ },
+ {
+ "name": "agentName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent name",
+ "defaultValue": "finops-hub-sre",
+ "toolTip": "Name of the Azure SRE Agent resource.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,61}[a-zA-Z0-9]$",
+ "validationMessage": "Name must be 3-63 characters and can contain letters, numbers, and hyphens. The first and last characters must be alphanumeric."
+ },
+ "visible": true
+ }
+ ],
+ "steps": [
+ {
+ "name": "configuration",
+ "label": "Configuration",
+ "elements": [
+ {
+ "name": "finopsHubKustoConnectorUri",
+ "type": "Microsoft.Common.TextBox",
+ "label": "FinOps hub Kusto URI",
+ "defaultValue": "",
+ "toolTip": "Optional database-qualified Azure Data Explorer URI for the FinOps hub. Example: https://cluster.region.kusto.windows.net/Hub",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^https://.+\\.kusto\\.windows\\.net/.+",
+ "validationMessage": "Enter a database-qualified Kusto URI such as https://cluster.region.kusto.windows.net/Hub, or leave blank."
+ },
+ "visible": true
+ },
+ {
+ "name": "finopsHubKustoClusterResourceId",
+ "type": "Microsoft.Common.TextBox",
+ "label": "FinOps hub Kusto cluster resource ID",
+ "defaultValue": "",
+ "toolTip": "Optional Azure Data Explorer cluster resource ID. Provide this to assign the agent managed identity AllDatabasesViewer on the cluster.",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft\\.Kusto/clusters/.+",
+ "validationMessage": "Enter a valid Microsoft.Kusto/clusters resource ID, or leave blank."
+ },
+ "visible": true
+ },
+ {
+ "name": "targetResourceGroups",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Additional target resource groups",
+ "defaultValue": "",
+ "toolTip": "Optional comma-separated resource group names the agent can observe or act on. The agent resource group is always included.",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^[A-Za-z0-9_.()\\-]+(\\s*,\\s*[A-Za-z0-9_.()\\-]+)*$",
+ "validationMessage": "Enter a comma-separated list of resource group names without blank entries or trailing commas."
+ },
+ "visible": true
+ },
+ {
+ "name": "accessLevel",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Access level",
+ "defaultValue": "High",
+ "toolTip": "High adds Contributor on target resource groups for autonomous remediation workflows. Low grants read-only monitoring roles.",
+ "constraints": {
+ "allowedValues": [
+ {
+ "label": "High",
+ "value": "High"
+ },
+ {
+ "label": "Low",
+ "value": "Low"
+ }
+ ],
+ "required": true
+ },
+ "visible": true
+ },
+ {
+ "name": "actionMode",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Action mode",
+ "defaultValue": "autonomous",
+ "toolTip": "Review requires approval before write actions. Autonomous lets the agent run enabled actions within its assigned access.",
+ "constraints": {
+ "allowedValues": [
+ {
+ "label": "Autonomous",
+ "value": "autonomous"
+ },
+ {
+ "label": "Review",
+ "value": "review"
+ },
+ {
+ "label": "Read only",
+ "value": "readOnly"
+ }
+ ],
+ "required": true
+ },
+ "visible": true
+ },
+ {
+ "name": "enableSubscriptionReaderRole",
+ "type": "Microsoft.Common.CheckBox",
+ "label": "Grant subscription Reader to the agent",
+ "toolTip": "Recommended. Grants the agent managed identity Reader on the deployment subscription for inventory, Resource Graph, capacity, quota, and monitoring coverage workflows.",
+ "defaultValue": true,
+ "visible": true
+ }
+ ]
+ }
+ ],
+ "outputs": {
+ "resourceGroupName": "[basics('resourceGroupName')]",
+ "agentName": "[basics('agentName')]",
+ "location": "[location()]",
+ "targetResourceGroupNames": "[steps('configuration').targetResourceGroups]",
+ "finopsHubKustoConnectorUri": "[steps('configuration').finopsHubKustoConnectorUri]",
+ "finopsHubKustoClusterResourceId": "[steps('configuration').finopsHubKustoClusterResourceId]",
+ "accessLevel": "[steps('configuration').accessLevel]",
+ "actionMode": "[steps('configuration').actionMode]",
+ "enableSubscriptionReaderRole": "[steps('configuration').enableSubscriptionReaderRole]"
+ }
+ }
+}
diff --git a/docs/deploy/sre-agent/latest/sre-agent-recipe.zip b/docs/deploy/sre-agent/latest/sre-agent-recipe.zip
new file mode 100644
index 000000000..159ae7454
Binary files /dev/null and b/docs/deploy/sre-agent/latest/sre-agent-recipe.zip differ
diff --git a/src/powershell/Tests/Unit/Templates.SreAgent.Deploy.Tests.ps1 b/src/powershell/Tests/Unit/Templates.SreAgent.Deploy.Tests.ps1
new file mode 100644
index 000000000..736b363c4
--- /dev/null
+++ b/src/powershell/Tests/Unit/Templates.SreAgent.Deploy.Tests.ps1
@@ -0,0 +1,1286 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+Describe 'SRE Agent deploy template' {
+ BeforeAll {
+ $script:RepoRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName
+ $script:DeployScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/deploy.sh'
+ $script:ApplyExtrasScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/apply-extras.sh'
+ $script:PostProvisionScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/post-provision.sh'
+ $script:VerifyScript = Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh'
+ $script:RecipeDir = Join-Path $script:RepoRoot 'src/templates/sre-agent/recipes/finops-hub'
+ $script:ScheduledTaskDir = Join-Path $script:RecipeDir 'automations/scheduled-tasks'
+ $script:OutputStylePath = Join-Path $script:RepoRoot 'src/templates/claude-plugin/output-styles/ftk-output-style.md'
+ $script:ReadmePath = Join-Path $script:RepoRoot 'src/templates/sre-agent/README.md'
+ $script:DocsPath = Join-Path $script:RepoRoot 'docs-mslearn/toolkit/sre-agent/deploy.md'
+ $script:AgentJsonPath = Join-Path $script:RecipeDir 'agent.json'
+ $script:SkipBash = -not (Get-Command bash -ErrorAction SilentlyContinue)
+
+ function Invoke-BashCommand {
+ param(
+ [Parameter(Mandatory)]
+ [string] $Command
+ )
+
+ Push-Location $script:RepoRoot
+ try {
+ $output = & bash -lc "MSYS2_ARG_CONV_EXCL='/subscriptions;/providers' $Command" 2>&1
+ [pscustomobject]@{
+ ExitCode = $LASTEXITCODE
+ Output = ($output -join "`n")
+ }
+ }
+ finally {
+ Pop-Location
+ }
+ }
+
+ function Invoke-BashCommandWithPath {
+ param(
+ [Parameter(Mandatory)]
+ [string] $Command,
+
+ [Parameter(Mandatory)]
+ [string] $PathPrefix
+ )
+
+ Push-Location $script:RepoRoot
+ try {
+ $originalPath = $env:PATH
+ $bashPathPrefix = ConvertTo-BashPath $PathPrefix
+ $quotedPathPrefix = ConvertTo-BashSingleQuoted $bashPathPrefix
+ $output = & bash -c "MSYS2_ARG_CONV_EXCL='/subscriptions;/providers' PATH=${quotedPathPrefix}:`$PATH $Command" 2>&1
+ [pscustomobject]@{
+ ExitCode = $LASTEXITCODE
+ Output = ($output -join "`n")
+ }
+ }
+ finally {
+ $env:PATH = $originalPath
+ Pop-Location
+ }
+ }
+
+ function ConvertTo-BashPath {
+ param(
+ [Parameter(Mandatory)]
+ [string] $Path
+ )
+
+ $quotedPath = ConvertTo-BashSingleQuoted $Path
+ $converted = & bash -lc "command -v cygpath >/dev/null 2>&1 && cygpath -u $quotedPath" 2>$null
+ if ($LASTEXITCODE -eq 0 -and $converted) {
+ return ($converted -join '')
+ }
+
+ return $Path
+ }
+
+ function ConvertTo-BashSingleQuoted {
+ param(
+ [Parameter(Mandatory)]
+ [string] $Value
+ )
+
+ return "'" + ($Value -replace "'", "'\''") + "'"
+ }
+
+ function Set-BashStub {
+ param(
+ [Parameter(Mandatory)]
+ [string] $Path,
+
+ [Parameter(Mandatory)]
+ [string] $Content
+ )
+
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::WriteAllText($Path, ($Content -replace "`r`n", "`n"), $utf8NoBom)
+ $bashPath = ConvertTo-BashPath $Path
+ $quotedPath = ConvertTo-BashSingleQuoted $bashPath
+ & bash -c "chmod +x $quotedPath"
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to mark bash stub executable: $Path"
+ }
+ }
+
+ function Set-PythonExtrasStub {
+ param(
+ [Parameter(Mandatory)]
+ [string] $BinDir
+ )
+
+ $realPython = (Get-Command python3 -ErrorAction SilentlyContinue).Source
+ if (-not $realPython) {
+ $realPython = (Get-Command python -ErrorAction Stop).Source
+ }
+ $realPython = ConvertTo-BashPath $realPython
+ $realPythonLiteral = ConvertTo-BashSingleQuoted $realPython
+
+ $stubContent = @'
+#!/usr/bin/env bash
+set -euo pipefail
+real_python=__REAL_PYTHON__
+
+if [[ "${1:-}" == "-c" && "${2:-}" == "import yaml" ]]; then
+ exit 0
+fi
+
+if [[ "${1:-}" == */build-extras.py ]]; then
+ output=""
+ while [[ "$#" -gt 0 ]]; do
+ case "$1" in
+ --output)
+ output="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ [[ -n "$output" ]] || exit 2
+ mkdir -p "$(dirname "$output")"
+ cat > "$output" <<'JSON'
+{
+ "connectors": [],
+ "builtInTools": { "overrides": [] },
+ "knowledgeItems": [],
+ "tools": [],
+ "skills": [],
+ "subagents": [
+ { "metadata": { "name": "chief-financial-officer" }, "spec": { "description": "test" } },
+ { "metadata": { "name": "ftk-database-query" }, "spec": { "description": "test" } },
+ { "metadata": { "name": "ftk-hubs-agent" }, "spec": { "description": "test" } },
+ { "metadata": { "name": "finops-practitioner" }, "spec": { "description": "test" } }
+ ],
+ "scheduledTasks": []
+}
+JSON
+ printf '{"connectors":0,"knowledgeItems":0,"tools":0,"skills":0,"subagents":4,"scheduledTasks":0}\n'
+ exit 0
+fi
+
+exec "$real_python" "$@"
+'@
+ $stubContent = $stubContent.Replace('__REAL_PYTHON__', $realPythonLiteral)
+
+ Set-BashStub -Path (Join-Path $BinDir 'python3') -Content $stubContent
+ Set-BashStub -Path (Join-Path $BinDir 'python') -Content $stubContent
+ }
+ }
+
+ Context 'bash availability' {
+ It 'has bash available for hermetic tests' {
+ if ($script:SkipBash) {
+ Set-ItResult -Skipped -Because 'bash is unavailable'
+ }
+ $true | Should -BeTrue
+ }
+ }
+
+ Context 'help and parsing' {
+ It 'prints help with portal labels and required flags' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --help"
+ $result.ExitCode | Should -Be 0
+ $result.Output | Should -Match 'Resource group'
+ $result.Output | Should -Match 'Agent name'
+ $result.Output | Should -Match 'Region'
+ $result.Output | Should -Match 'Subscription'
+ $result.Output | Should -Match '--cluster-uri'
+ }
+
+ It 'documents the same help lines in README and docs' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $help = Invoke-BashCommand "bash '$script:DeployScript' --help"
+ $help.ExitCode | Should -Be 0
+ $readme = Get-Content -Path $script:ReadmePath -Raw
+ $docs = Get-Content -Path $script:DocsPath -Raw
+
+ @(
+ '--recipe ',
+ '--resource-group ',
+ '--name ',
+ '--location ',
+ '--subscription ',
+ '--cluster-uri ',
+ '--cluster-resource-id ',
+ '--deploy-name '
+ ) | ForEach-Object {
+ $escaped = [regex]::Escape($_)
+ $help.Output | Should -Match $escaped
+ $readme | Should -Match $escaped
+ $docs | Should -Match $escaped
+ }
+ }
+
+ It 'errors for each missing required recipe flag' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $cases = @(
+ @{
+ Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --dry-run"
+ Match = 'subscription'
+ }
+ @{
+ Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 --dry-run"
+ Match = 'resource-group'
+ }
+ @{
+ Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-x --dry-run"
+ Match = 'name'
+ }
+ @{
+ Command = "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-x -n a --dry-run"
+ Match = 'location'
+ }
+ )
+
+ foreach ($case in $cases) {
+ $result = Invoke-BashCommand $case.Command
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match $case.Match
+ }
+ }
+
+ It 'errors on unknown flags' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --recipes '$script:RecipeDir' --dry-run"
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match 'unknown flag'
+ }
+
+ It 'errors when a value-taking flag is missing its value' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' -g -n foo --dry-run"
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match 'requires a value'
+ }
+
+ It 'errors when verify-agent expected config is missing its value' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:VerifyScript' 00000000-0000-0000-0000-000000000000 rg-test test-agent --expected"
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match 'flag --expected requires a value'
+ $result.Output | Should -Not -Match 'unbound variable'
+ }
+
+ It 'rejects unknown verify-agent arguments before Azure calls' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:VerifyScript' 00000000-0000-0000-0000-000000000000 rg-test test-agent --bogus"
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match "unknown argument '--bogus'"
+ $result.Output | Should -Not -Match 'Could not resolve agent endpoint'
+ }
+
+ It 'rejects the positional footgun' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --dry-run rg-test"
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match 'unknown argument'
+ }
+
+ It 'shows customer values and not maintainer defaults on happy-path dry-run' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l westus3 --cluster-uri https://example.westus3.kusto.windows.net/Hub --cluster-resource-id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Kusto/clusters/fake --dry-run"
+ $result.ExitCode | Should -Be 0
+ $result.Output | Should -Match 'rg-test-customer'
+ $result.Output | Should -Match 'customer-sre-agent'
+ $result.Output | Should -Match 'westus3'
+ $result.Output | Should -Not -Match 'rg-finops-sre-agent|finops-sre-agent|eastus2'
+ }
+
+ It 'uses a deterministic default deployment name across dry-runs' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $deployRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-deploy-name-" + [guid]::NewGuid().ToString('N'))
+ try {
+ $command = "SRE_AGENT_DEPLOY_DIR='$deployRoot' bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l westus3 --cluster-uri https://example.westus3.kusto.windows.net/Hub --cluster-resource-id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Kusto/clusters/fake --dry-run"
+ $first = Invoke-BashCommand $command
+ $second = Invoke-BashCommand $command
+
+ $first.ExitCode | Should -Be 0
+ $second.ExitCode | Should -Be 0
+ $firstPath = [regex]::Match($first.Output, 'Parameters:\s+(.+)').Groups[1].Value.Trim()
+ $secondPath = [regex]::Match($second.Output, 'Parameters:\s+(.+)').Groups[1].Value.Trim()
+
+ $firstPath | Should -Be $secondPath
+ $firstPath | Should -Match 'sre-agent-customer-sre-agent-[a-f0-9]{12}'
+
+ $parameters = Get-Content -Path $firstPath -Raw | ConvertFrom-Json
+ $parameters.parameters.upgradeChannel.value | Should -Be 'Preview'
+ $parameters.parameters.experimentalSettings.value.EnableSandboxGroup | Should -BeTrue
+ $parameters.parameters.experimentalSettings.value.EnableWorkspaceTools | Should -BeTrue
+ $parameters.parameters.monthlyAgentUnitLimit.value | Should -Be 10000
+ }
+ finally {
+ Remove-Item -Path $deployRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ It 'requires cluster resource ID for dry-run when a Kusto URI is provided' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l westus3 --cluster-uri https://example.westus3.kusto.windows.net/Hub --dry-run"
+
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match '--cluster-resource-id is required with --cluster-uri for --dry-run'
+ }
+
+ It 'auto-resolves the Kusto cluster resource ID before deployment' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-kusto-resolve-" + [guid]::NewGuid().ToString('N'))
+ $binDir = Join-Path $tempRoot 'bin'
+ $deployRoot = Join-Path $tempRoot 'deploy'
+ New-Item -ItemType Directory -Force -Path $binDir, $deployRoot | Out-Null
+
+ $fakeAz = Join-Path $binDir 'az'
+ Set-BashStub -Path $fakeAz -Content @'
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "$*" == *"account get-access-token"* ]]; then
+ echo "fake-token"
+ exit 0
+fi
+
+if [[ "${1:-}" == "rest" ]]; then
+ echo "{}"
+ exit 0
+fi
+
+if [[ "$*" == *"resource list"* ]]; then
+ exit 0
+fi
+
+if [[ "$*" == *"graph query"* ]]; then
+ cat <<'JSON'
+{
+ "data": [
+ {
+ "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/example"
+ }
+ ]
+}
+JSON
+ exit 0
+fi
+
+if [[ "$*" == *"deployment sub show"* ]]; then
+ cat <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded",
+ "outputs": {
+ "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" },
+ "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" },
+ "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" }
+ }
+ }
+}
+JSON
+ exit 0
+fi
+
+if [[ "$*" == *"deployment sub create"* ]]; then
+ cat <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded",
+ "outputs": {
+ "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" },
+ "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" },
+ "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" }
+ }
+ }
+}
+JSON
+ exit 0
+fi
+
+case "$*" in
+ *"account show"*|*"account set"*|*"provider register"*|*"version"*)
+ exit 0
+ ;;
+esac
+
+exit 0
+'@
+
+ $fakeCurl = Join-Path $binDir 'curl'
+ Set-BashStub -Path $fakeCurl -Content @'
+#!/usr/bin/env bash
+set -euo pipefail
+
+out=""
+url=""
+method="GET"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -o)
+ out="$2"
+ shift 2
+ ;;
+ -w)
+ shift 2
+ ;;
+ -X)
+ method="$2"
+ shift 2
+ ;;
+ -H|--data-binary)
+ shift 2
+ ;;
+ http*)
+ url="$1"
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [[ "$url" == */api/v2/extendedAgent/connectors/finops-hub-kusto ]]; then
+ cat > "$out" <<'JSON'
+{"name":"finops-hub-kusto","properties":{"dataConnectorType":"Kusto"}}
+JSON
+ printf "201"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/connectors ]]; then
+ cat > "$out" <<'JSON'
+{
+ "value": [
+ { "name": "chart-artifact-verification-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "chart-artifact-verification.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "document-index-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "document-index.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "ftk-output-style-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "ftk-output-style.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "known-issues-and-workarounds-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "known-issues-and-workarounds.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "onboarding-recommendations-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "onboarding-recommendations.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "teams-notification-guide-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "teams-notification-guide.md", "createdAt": "2026-05-25T19:57:31Z" } } }
+ ]
+}
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/connectors/* ]]; then
+ cat > "$out" <<'JSON'
+{
+ "properties": {
+ "dataConnectorType": "KnowledgeFile",
+ "extendedProperties": {
+ "createdAt": "2026-05-25T19:57:31Z",
+ "lastModifiedAt": "2026-05-25T19:57:31Z"
+ }
+ }
+}
+JSON
+ if [[ "$method" == "PUT" ]]; then printf "201"; else printf "200"; fi
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/agent/tools/configure ]]; then
+ cat > "$out" <<'JSON'
+{}
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v1/scheduledtasks ]]; then
+ cat > "$out" <<'JSON'
+[]
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/agents/* ]]; then
+ [[ -n "${AGENT_APPLY_LOG:-}" ]] && basename "$url" >> "$AGENT_APPLY_LOG"
+ cat > "$out" <<'JSON'
+{
+ "type": "ExtendedAgent",
+ "properties": {
+ "provisioningState": "Succeeded"
+ }
+}
+JSON
+ printf "201"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/tools/* || "$url" == */api/v2/extendedAgent/skills/* || "$url" == */api/v2/extendedAgent/scheduledtasks/* ]]; then
+ cat > "$out" <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded"
+ }
+}
+JSON
+ printf "201"
+ exit 0
+fi
+
+cat > "$out" <<'JSON'
+{
+ "files": [
+ { "name": "chart-artifact-verification-md", "isIndexed": true, "errorReason": null },
+ { "name": "document-index-md", "isIndexed": true, "errorReason": null },
+ { "name": "ftk-output-style-md", "isIndexed": true, "errorReason": null },
+ { "name": "known-issues-and-workarounds-md", "isIndexed": true, "errorReason": null },
+ { "name": "onboarding-recommendations-md", "isIndexed": true, "errorReason": null },
+ { "name": "teams-notification-guide-md", "isIndexed": true, "errorReason": null }
+ ],
+ "continuationToken": ""
+}
+JSON
+printf "200"
+'@
+
+ Set-PythonExtrasStub -BinDir $binDir
+
+ Set-BashStub -Path (Join-Path $binDir 'sleep') -Content @'
+#!/usr/bin/env bash
+exit 0
+'@
+
+ try {
+ $command = "SRE_AGENT_DEPLOY_DIR='$deployRoot' SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS=0 SRE_AGENT_APPLY_RETRY_DELAY_SECONDS=0 bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l eastus2 --cluster-uri https://example.westus3.kusto.windows.net/Hub"
+ $result = Invoke-BashCommandWithPath $command $binDir
+ $result.ExitCode | Should -Be 0 -Because $result.Output
+ $result.Output | Should -Match 'Resolving Kusto cluster resource ID from --cluster-uri'
+ $result.Output | Should -Match '/providers/Microsoft\.Kusto/clusters/example'
+
+ $parametersFile = (Get-ChildItem -Path $deployRoot -Recurse -Filter 'deploy.parameters.json' | Select-Object -First 1).FullName
+ $parametersFile | Should -Not -BeNullOrEmpty
+ $parameters = Get-Content -Path $parametersFile -Raw | ConvertFrom-Json
+ $parameters.parameters.finopsHubKustoClusterResourceId.value | Should -Be '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/example'
+ }
+ finally {
+ Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ It 'warns but continues when the Kusto cluster denies public query access' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-kusto-private-" + [guid]::NewGuid().ToString('N'))
+ $binDir = Join-Path $tempRoot 'bin'
+ $deployRoot = Join-Path $tempRoot 'deploy'
+ New-Item -ItemType Directory -Force -Path $binDir, $deployRoot | Out-Null
+
+ $fakeAz = Join-Path $binDir 'az'
+ Set-BashStub -Path $fakeAz -Content @'
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "$*" == *"account get-access-token"* ]]; then
+ echo "fake-token"
+ exit 0
+fi
+
+if [[ "${1:-}" == "rest" ]]; then
+ echo "{}"
+ exit 0
+fi
+
+if [[ "$*" == *"resource show"* ]]; then
+ cat <<'JSON'
+{
+ "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/privateadx",
+ "properties": {
+ "uri": "https://privateadx.westus.kusto.windows.net",
+ "publicNetworkAccess": "Disabled",
+ "privateEndpointConnections": [
+ {
+ "name": "privateadx-ep",
+ "properties": {
+ "privateLinkServiceConnectionState": {
+ "status": "Approved"
+ }
+ }
+ }
+ ]
+ }
+}
+JSON
+ exit 0
+fi
+
+if [[ "$*" == *"deployment sub show"* ]]; then
+ cat <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded",
+ "outputs": {
+ "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" },
+ "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" },
+ "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" }
+ }
+ }
+}
+JSON
+ exit 0
+fi
+
+if [[ "$*" == *"deployment sub create"* ]]; then
+ cat <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded",
+ "outputs": {
+ "SRE_AGENT_ENDPOINT": { "value": "https://example.azuresre.ai" },
+ "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID": { "value": "00000000-0000-0000-0000-000000000001" },
+ "AGENT_PORTAL_URL": { "value": "https://sre.azure.com/#/agent/00000000-0000-0000-0000-000000000000/rg-test-customer/customer-sre-agent" }
+ }
+ }
+}
+JSON
+ exit 0
+fi
+
+case "$*" in
+ *"account show"*|*"account set"*|*"provider register"*|*"version"*)
+ exit 0
+ ;;
+esac
+
+exit 0
+'@
+
+ $fakeCurl = Join-Path $binDir 'curl'
+ Set-BashStub -Path $fakeCurl -Content @'
+#!/usr/bin/env bash
+set -euo pipefail
+
+out=""
+url=""
+method="GET"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -o)
+ out="$2"
+ shift 2
+ ;;
+ -w)
+ shift 2
+ ;;
+ -X)
+ method="$2"
+ shift 2
+ ;;
+ -H|--data-binary)
+ shift 2
+ ;;
+ http*)
+ url="$1"
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [[ "$url" == */api/v2/extendedAgent/connectors/finops-hub-kusto ]]; then
+ cat > "$out" <<'JSON'
+{"name":"finops-hub-kusto","properties":{"dataConnectorType":"Kusto"}}
+JSON
+ printf "201"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/connectors ]]; then
+ cat > "$out" <<'JSON'
+{
+ "value": [
+ { "name": "chart-artifact-verification-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "chart-artifact-verification.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "document-index-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "document-index.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "ftk-output-style-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "ftk-output-style.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "known-issues-and-workarounds-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "known-issues-and-workarounds.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "onboarding-recommendations-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "onboarding-recommendations.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "teams-notification-guide-md", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "teams-notification-guide.md", "createdAt": "2026-05-25T19:57:31Z" } } }
+ ]
+}
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/connectors/* ]]; then
+ cat > "$out" <<'JSON'
+{
+ "properties": {
+ "dataConnectorType": "KnowledgeFile",
+ "extendedProperties": {
+ "createdAt": "2026-05-25T19:57:31Z",
+ "lastModifiedAt": "2026-05-25T19:57:31Z"
+ }
+ }
+}
+JSON
+ if [[ "$method" == "PUT" ]]; then printf "201"; else printf "200"; fi
+ exit 0
+fi
+
+if [[ "$url" == */api/v1/scheduledtasks ]]; then
+ cat > "$out" <<'JSON'
+[]
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/agent/tools/configure ]]; then
+ cat > "$out" <<'JSON'
+{}
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/agents/* || "$url" == */api/v2/extendedAgent/tools/* || "$url" == */api/v2/extendedAgent/skills/* || "$url" == */api/v2/extendedAgent/scheduledtasks/* ]]; then
+ cat > "$out" <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded"
+ }
+}
+JSON
+ printf "201"
+ exit 0
+fi
+
+cat > "$out" <<'JSON'
+{}
+JSON
+printf "200"
+'@
+
+ Set-PythonExtrasStub -BinDir $binDir
+
+ Set-BashStub -Path (Join-Path $binDir 'sleep') -Content @'
+#!/usr/bin/env bash
+exit 0
+'@
+
+ try {
+ $clusterId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-hub/providers/Microsoft.Kusto/clusters/privateadx'
+ $command = "SRE_AGENT_DEPLOY_DIR='$deployRoot' SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS=0 SRE_AGENT_APPLY_RETRY_DELAY_SECONDS=0 bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test-customer -n customer-sre-agent -l eastus2 --cluster-uri https://privateadx.westus.kusto.windows.net/Hub --cluster-resource-id $clusterId"
+ $result = Invoke-BashCommandWithPath $command $binDir
+ $result.ExitCode | Should -Be 0 -Because $result.Output
+ $result.Output | Should -Match 'Warning: The Kusto cluster denies public query access'
+ $result.Output | Should -Match 'private endpoint ADX blocks direct KQL queries'
+ $result.Output | Should -Match 'https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations'
+ $result.Output | Should -Match 'SRE Agent ready'
+ }
+ finally {
+ Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ It 'routes deploy through the copied starter-lab infra and apply-extras path' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $deployContent = Get-Content -Path $script:DeployScript -Raw
+ $deployContent | Should -Match 'INFRA_DIR=.*infra'
+ $deployContent | Should -Match '\$\{INFRA_DIR\}/main\.bicep'
+ $deployContent | Should -Match 'apply-extras\.sh'
+ $deployContent | Should -Not -Match 'bicep/assemble-agent|hydrate-extensions'
+
+ Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') | Should -BeTrue
+ Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/apply-extras.sh') | Should -BeTrue
+ }
+ }
+
+ Context 'repo invariants' {
+ It 'removes shipped recipe identity defaults' {
+ $agentJson = Get-Content -Path $script:AgentJsonPath -Raw
+ $agentJson | Should -Not -Match '"identity"'
+ }
+
+ It 'uses the current SRE Agent API, model provider, and sandbox configuration' {
+ $agentJson = Get-Content -Path $script:AgentJsonPath -Raw | ConvertFrom-Json
+ $mainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') -Raw
+ $sreAgentBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/modules/sre-agent.bicep') -Raw
+ $verifyScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh') -Raw
+
+ $agentJson.upgradeChannel | Should -Be 'Preview'
+ $agentJson.defaultModelProvider | Should -Be 'MicrosoftFoundry'
+ $agentJson.defaultModelName | Should -Be 'Automatic'
+ $agentJson.experimentalSettings.EnableSandboxGroup | Should -BeTrue
+ $agentJson.experimentalSettings.EnableWorkspaceTools | Should -BeTrue
+ $sreAgentBicep | Should -Match 'Microsoft\.App/agents@2026-01-01'
+ $sreAgentBicep | Should -Not -Match 'Microsoft\.App/agents@2025-05-01-preview'
+ $sreAgentBicep | Should -Match 'defaultModel:'
+ $sreAgentBicep | Should -Match 'provider: defaultModelProvider'
+ $sreAgentBicep | Should -Match 'name: defaultModelName'
+ $sreAgentBicep | Should -Match 'upgradeChannel: upgradeChannel'
+ $mainBicep | Should -Match 'defaultModelProvider'
+ $mainBicep | Should -Match 'defaultModelName'
+ $mainBicep | Should -Match 'EnableSandboxGroup'
+ $mainBicep | Should -Match 'EnableWorkspaceTools'
+ $verifyScript | Should -Match 'API_VERSION="2026-01-01"'
+ }
+
+ It 'limits legacy config env-var references to the allowlist' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "git grep -nE 'FINOPS_HUB_CLUSTER_URI|FINOPS_HUB_CLUSTER_RESOURCE_ID' -- docs-mslearn/toolkit/sre-agent/deploy.md src/templates/sre-agent"
+ $result.ExitCode | Should -Be 0
+
+ $paths = $result.Output -split "`n" |
+ Where-Object { $_ } |
+ ForEach-Object { ($_ -split ':', 2)[0] } |
+ Sort-Object -Unique
+
+ $paths | Should -Be @(
+ 'docs-mslearn/toolkit/sre-agent/deploy.md',
+ 'src/templates/sre-agent/bin/build-extras.py',
+ 'src/templates/sre-agent/README.md',
+ 'src/templates/sre-agent/recipes/finops-hub/connectors.json'
+ )
+ }
+
+ It 'requires recipe package hash validation before extraction' {
+ $applyExtrasScriptPath = Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1'
+ $applyExtrasLines = Get-Content -Path $applyExtrasScriptPath
+ $applyExtrasScript = $applyExtrasLines -join "`n"
+
+ $applyExtrasScript | Should -Match "\$recipePackageSha256 = Get-RequiredEnv 'recipePackageSha256'"
+ $applyExtrasScript | Should -Match 'Get-FileHash\s+-Algorithm\s+SHA256'
+ $applyExtrasScript | Should -Match '(?s)recipePackageSha256.*throw'
+
+ $hashLine = for ($i = 0; $i -lt $applyExtrasLines.Count; $i++) {
+ if ($applyExtrasLines[$i] -match 'Get-FileHash' -and $applyExtrasLines[$i] -match 'SHA256') {
+ $i + 1
+ break
+ }
+ }
+ $throwLine = for ($i = 0; $i -lt $applyExtrasLines.Count; $i++) {
+ if ($applyExtrasLines[$i] -match 'throw' -and $applyExtrasLines[$i] -match 'hash|SHA256|recipePackageSha256') {
+ $i + 1
+ break
+ }
+ }
+ $expandArchiveLine = for ($i = 0; $i -lt $applyExtrasLines.Count; $i++) {
+ if ($applyExtrasLines[$i] -match 'Expand-Archive') {
+ $i + 1
+ break
+ }
+ }
+
+ $hashLine | Should -BeGreaterThan 0
+ $throwLine | Should -BeGreaterThan 0
+ $expandArchiveLine | Should -BeGreaterThan 0
+ $hashLine | Should -BeLessThan $expandArchiveLine
+ $throwLine | Should -BeLessThan $expandArchiveLine
+ }
+
+ It 'declares recipePackageSha256 in the compiled deploy templates' {
+ foreach ($compiledPath in @(
+ (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/latest/azuredeploy.json'),
+ (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/14.0/azuredeploy.json')
+ )) {
+ $compiledTemplate = Get-Content -Path $compiledPath -Raw | ConvertFrom-Json -Depth 100
+ $compiledTemplate.parameters.PSObject.Properties.Name | Should -Contain 'recipePackageSha256'
+ }
+ }
+
+ It 'requires an https allowlisted recipe package origin before download' {
+ $applyExtrasScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1') -Raw
+
+ $applyExtrasScript | Should -Match '(?is)\[uri\]\$[A-Za-z0-9_]+\s*=\s*\$recipePackageUri'
+ $applyExtrasScript | Should -Match "(?is)\.Scheme\s*-ne\s*'https'.*throw"
+ $applyExtrasScript | Should -Match '(?is)\.Host.*(-notcontains|-notin|-notmatch|-notlike).*(throw|fail)'
+ }
+
+ It 'keeps connectors.secrets.env out of scripts' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "git grep -nE 'connectors\.secrets\.env' -- src/templates/sre-agent/bin src/templates/sre-agent/infra"
+ $result.ExitCode | Should -Be 1
+ }
+
+ It 'removes the legacy custom bicep deployment surface' {
+ Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bicep') | Should -BeFalse
+ Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/hydrate-extensions.sh') | Should -BeFalse
+ }
+
+ It 'removes dead telemetry scripts and source references from the SRE Agent template' {
+ Test-Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/telemetry.sh') | Should -BeFalse
+
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "git grep -nE 'f10eff7f-b995-4c41-8347-90f0f55d5969|send_telemetry' -- src/templates/sre-agent"
+ $result.ExitCode | Should -Be 1 -Because $result.Output
+ }
+
+ It 'removes legacy telemetry flags and env vars from deploy surfaces' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $deployContent = Get-Content -Path $script:DeployScript -Raw
+ $deployContent | Should -Not -Match '--no-telemetry'
+
+ $result = Invoke-BashCommand "git grep -nE '\-\-no-telemetry|SRE_AGENT_NO_TELEMETRY' -- docs-mslearn/toolkit/sre-agent/deploy.md src/templates/sre-agent"
+ $result.ExitCode | Should -Be 1 -Because $result.Output
+ }
+
+ It 'parameterizes the deployer principal type in the SRE Agent module' {
+ $sreAgentBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/modules/sre-agent.bicep') -Raw
+
+ $sreAgentBicep | Should -Match "(?s)@allowed\(\s*\[\s*'User'\s*,\s*'ServicePrincipal'\s*\]\s*\)\s*param\s+deployerPrincipalType\s+string"
+ $sreAgentBicep | Should -Match 'principalType:\s*deployerPrincipalType'
+ $sreAgentBicep | Should -Not -Match "principalType:\s*'User'"
+ }
+
+ It 'passes ServicePrincipal deployer principal type in the CI/CD example' {
+ $githubActionsDeploy = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml') -Raw
+
+ $githubActionsDeploy | Should -Match 'ServicePrincipal'
+ $githubActionsDeploy | Should -Match 'deployerPrincipalType|--deployer-principal-type'
+ }
+
+ It 'uses deterministic subscription and resource group identity for support resource names' {
+ $mainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') -Raw
+ $resourcesBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/resources.bicep') -Raw
+
+ $mainBicep | Should -Match "agentResourceGroupId = subscriptionResourceId\('Microsoft\.Resources/resourceGroups', resourceGroupName\)"
+ $mainBicep | Should -Match 'namingSeed = toLower'
+ $mainBicep | Should -Match 'subscription\(\)\.subscriptionId'
+ $mainBicep | Should -Match 'agentResourceGroupId'
+ $mainBicep | Should -Match 'agentName'
+
+ $resourcesBicep | Should -Match 'param namingSeed string'
+ $resourcesBicep | Should -Match 'uniqueSuffix = uniqueString\(namingSeed\)'
+ $resourcesBicep | Should -Not -Match 'resourceGroup\(\)\.id|deployment\(\)\.name'
+ }
+
+ It 'defaults to read-only identity with autonomous reporting' {
+ $portalMainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/main.bicep') -Raw
+ $cliMainBicep = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/infra/main.bicep') -Raw
+ $createUiDef = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/createUiDefinition.json') -Raw | ConvertFrom-Json -Depth 100
+ $deployScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/deploy.sh') -Raw
+ $recipeAgentJson = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/recipes/finops-hub/agent.json') -Raw | ConvertFrom-Json -Depth 100
+ $expectedConfigJson = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/recipes/finops-hub/expected-config.json') -Raw | ConvertFrom-Json -Depth 100
+
+ $portalMainBicep | Should -Match "param accessLevel string = 'Low'"
+ $portalMainBicep | Should -Match 'param enableSubscriptionReaderRole bool = false'
+ $portalMainBicep | Should -Match "param actionMode string = 'autonomous'"
+
+ $cliMainBicep | Should -Match "param accessLevel string = 'Low'"
+ $cliMainBicep | Should -Match 'param enableSubscriptionReaderRole bool = false'
+ $cliMainBicep | Should -Match "param actionMode string = 'autonomous'"
+
+ $accessLevelControl = $createUiDef.parameters.steps |
+ Where-Object { $_.name -eq 'configuration' } |
+ Select-Object -First 1 -ExpandProperty elements |
+ Where-Object { $_.name -eq 'accessLevel' }
+ $accessLevelControl.defaultValue | Should -Be 'Low'
+
+ $subscriptionReaderControl = $createUiDef.parameters.steps |
+ Where-Object { $_.name -eq 'configuration' } |
+ Select-Object -First 1 -ExpandProperty elements |
+ Where-Object { $_.name -eq 'enableSubscriptionReaderRole' }
+ $subscriptionReaderControl.defaultValue | Should -Be $false
+
+ $actionModeControl = $createUiDef.parameters.steps |
+ Where-Object { $_.name -eq 'configuration' } |
+ Select-Object -First 1 -ExpandProperty elements |
+ Where-Object { $_.name -eq 'actionMode' }
+ $actionModeControl.defaultValue | Should -Be 'autonomous'
+
+ $deployScript | Should -Match 'ENABLE_SUBSCRIPTION_READER="false"'
+
+ $recipeAgentJson.access.accessLevel | Should -Be 'Low'
+ $recipeAgentJson.access.actionMode | Should -Be 'autonomous'
+
+ $expectedConfigJson.agent.accessLevel | Should -Be 'Low'
+ $expectedConfigJson.agent.actionMode | Should -Be 'autonomous'
+ $expectedConfigJson.subscriptionRoleAssignments | Should -Be @()
+ }
+
+ It 'declares Low accessLevel and disabled subscription Reader in the compiled deploy templates' {
+ foreach ($compiledPath in @(
+ (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/latest/azuredeploy.json'),
+ (Join-Path $script:RepoRoot 'docs/deploy/sre-agent/14.0/azuredeploy.json')
+ )) {
+ $compiledTemplate = Get-Content -Path $compiledPath -Raw | ConvertFrom-Json -Depth 100
+ $compiledTemplate.parameters.accessLevel.defaultValue | Should -Be 'Low'
+ $compiledTemplate.parameters.enableSubscriptionReaderRole.defaultValue | Should -Be $false
+ }
+ }
+
+ It 'applies subagents after their local handoff targets' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+
+ $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sre-agent-post-provision-" + [guid]::NewGuid().ToString('N'))
+ $binDir = Join-Path $tempRoot 'bin'
+ $buildDir = Join-Path $tempRoot 'build'
+ $logPath = Join-Path $tempRoot 'agent-apply.log'
+ New-Item -ItemType Directory -Force -Path $binDir, $buildDir | Out-Null
+
+ $fakeAz = Join-Path $binDir 'az'
+ Set-BashStub -Path $fakeAz -Content @'
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "$*" == *"account get-access-token"* ]]; then
+ echo "fake-token"
+ exit 0
+fi
+
+if [[ "${1:-}" == "rest" ]]; then
+ echo "{}"
+ exit 0
+fi
+
+case "$*" in
+ *"account show"*|*"account set"*|*"version"*)
+ exit 0
+ ;;
+esac
+
+exit 1
+'@
+
+ $fakeCurl = Join-Path $binDir 'curl'
+ Set-BashStub -Path $fakeCurl -Content @'
+#!/usr/bin/env bash
+set -euo pipefail
+
+out=""
+url=""
+method="GET"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -o)
+ out="$2"
+ shift 2
+ ;;
+ -w)
+ shift 2
+ ;;
+ -X)
+ method="$2"
+ shift 2
+ ;;
+ -H|--data-binary)
+ shift 2
+ ;;
+ http*)
+ url="$1"
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [[ "$url" == */api/v2/extendedAgent/connectors ]]; then
+ cat > "$out" <<'JSON'
+{
+ "value": [
+ { "name": "chart-artifact-verification-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "chart-artifact-verification.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "document-index-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "document-index.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "ftk-output-style-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "ftk-output-style.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "known-issues-and-workarounds-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "known-issues-and-workarounds.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "onboarding-recommendations-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "onboarding-recommendations.md", "createdAt": "2026-05-25T19:57:31Z" } } },
+ { "name": "teams-notification-guide-md", "type": "KnowledgeItem", "properties": { "dataConnectorType": "KnowledgeFile", "extendedProperties": { "displayName": "teams-notification-guide.md", "createdAt": "2026-05-25T19:57:31Z" } } }
+ ]
+}
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/connectors/* ]]; then
+ cat > "$out" <<'JSON'
+{
+ "type": "KnowledgeItem",
+ "properties": {
+ "dataConnectorType": "KnowledgeFile",
+ "extendedProperties": {
+ "createdAt": "2026-05-25T19:57:31Z",
+ "lastModifiedAt": "2026-05-25T19:57:31Z"
+ }
+ }
+}
+JSON
+ if [[ "$method" == "PUT" ]]; then printf "201"; else printf "200"; fi
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/agents/* ]]; then
+ [[ -n "${AGENT_APPLY_LOG:-}" ]] && basename "$url" >> "$AGENT_APPLY_LOG"
+ cat > "$out" <<'JSON'
+{
+ "type": "ExtendedAgent",
+ "properties": {
+ "provisioningState": "Succeeded"
+ }
+}
+JSON
+ printf "201"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/extendedAgent/tools/* || "$url" == */api/v2/extendedAgent/skills/* || "$url" == */api/v2/extendedAgent/scheduledtasks/* ]]; then
+ cat > "$out" <<'JSON'
+{
+ "properties": {
+ "provisioningState": "Succeeded"
+ }
+}
+JSON
+ printf "201"
+ exit 0
+fi
+
+if [[ "$url" == */api/v2/agent/tools/configure ]]; then
+ cat > "$out" <<'JSON'
+{}
+JSON
+ printf "200"
+ exit 0
+fi
+
+if [[ "$url" == */api/v1/scheduledtasks ]]; then
+ cat > "$out" <<'JSON'
+[]
+JSON
+ printf "200"
+ exit 0
+fi
+
+cat > "$out" <<'JSON'
+{
+ "files": [
+ { "name": "chart-artifact-verification-md", "isIndexed": true, "errorReason": null },
+ { "name": "document-index-md", "isIndexed": true, "errorReason": null },
+ { "name": "ftk-output-style-md", "isIndexed": true, "errorReason": null },
+ { "name": "known-issues-and-workarounds-md", "isIndexed": true, "errorReason": null },
+ { "name": "onboarding-recommendations-md", "isIndexed": true, "errorReason": null },
+ { "name": "teams-notification-guide-md", "isIndexed": true, "errorReason": null }
+ ],
+ "continuationToken": ""
+}
+JSON
+printf "200"
+'@
+
+ Set-PythonExtrasStub -BinDir $binDir
+
+ Set-BashStub -Path (Join-Path $binDir 'sleep') -Content @'
+#!/usr/bin/env bash
+exit 0
+'@
+
+ try {
+ $command = "AGENT_APPLY_LOG='$logPath' SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS=0 SRE_AGENT_APPLY_RETRY_DELAY_SECONDS=0 bash '$script:PostProvisionScript' --endpoint https://example.azuresre.ai --subscription 00000000-0000-0000-0000-000000000000 --resource-group rg-test-customer --name customer-sre-agent --recipe '$script:RecipeDir' --build-dir '$buildDir'"
+ $result = Invoke-BashCommandWithPath $command $binDir
+ $result.ExitCode | Should -Be 0 -Because $result.Output
+
+ $order = @(Get-Content -Path $logPath)
+ [array]::IndexOf($order, 'chief-financial-officer') | Should -BeLessThan ([array]::IndexOf($order, 'finops-practitioner'))
+ [array]::IndexOf($order, 'ftk-database-query') | Should -BeLessThan ([array]::IndexOf($order, 'finops-practitioner'))
+ [array]::IndexOf($order, 'ftk-hubs-agent') | Should -BeLessThan ([array]::IndexOf($order, 'finops-practitioner'))
+ }
+ finally {
+ Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ It 'derives expected recipe connectors for verification' {
+ $verifyScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh') -Raw
+
+ $verifyScript | Should -Match 'connectors\.json'
+ $verifyScript | Should -Match 'EXPECTED_CONNECTORS'
+ $verifyScript | Should -Match 'EXP_CONN_CT=.*EXPECTED_CONNECTORS'
+ $verifyScript | Should -Match 'EXP_CONN_NAMES=.*EXPECTED_CONNECTORS'
+ }
+
+ It 'uploads the shared FinOps output style as a portal knowledge source' {
+ $applyExtrasScript = Get-Content -Path $script:ApplyExtrasScript -Raw
+ $buildExtrasScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/build-extras.py') -Raw
+
+ Test-Path $script:OutputStylePath | Should -BeTrue
+ $buildExtrasScript | Should -Match 'claude-plugin/output-styles/ftk-output-style\.md'
+ $buildExtrasScript | Should -Match 'Output style knowledge document not found'
+ $applyExtrasScript | Should -Match 'knowledgeItems'
+ $applyExtrasScript | Should -Match '/api/v2/extendedAgent/connectors'
+ $applyExtrasScript | Should -Match 'Knowledge sources failed to index'
+ $applyExtrasScript | Should -Match 'KnowledgeFile'
+ $applyExtrasScript | Should -Not -Match ('sr' + 'ectl')
+ $applyExtrasScript | Should -Not -Match '/api/v1/agentmemory/files'
+ }
+
+ It 'requires expected knowledge sources and verifies indexing' {
+ $expectedConfig = Get-Content -Path (Join-Path $script:RecipeDir 'expected-config.json') -Raw | ConvertFrom-Json
+ $verifyScript = Get-Content -Path $script:VerifyScript -Raw
+
+ $expectedConfig.knowledgeSources.Count | Should -Be 6
+ $expectedConfig.knowledgeSources | Should -Contain 'ftk-output-style.md'
+ $expectedConfig.PSObject.Properties.Name | Should -Not -Contain 'knowledgeDocs'
+ $verifyScript | Should -Match '/api/v2/extendedAgent/connectors'
+ $verifyScript | Should -Match '\$expected\[\] \| \. as \$name \| select\(\$actual \| index\(\$name\)\)'
+ $verifyScript | Should -Match 'Knowledge sources expected'
+ $verifyScript | Should -Match 'Knowledge sources indexed'
+ $verifyScript | Should -Match 'Unindexed knowledge sources'
+ $verifyScript | Should -Not -Match '/api/v1/agentmemory'
+ $verifyScript | Should -Not -Match '"knowledge_" \+'
+ $verifyScript | Should -Not -Match 'Knowledge connector rows'
+ }
+
+ It 'does not duplicate response plan verification with dead incident-filter scan state' {
+ $verifyScript = Get-Content -Path $script:VerifyScript -Raw
+
+ ([regex]::Matches($verifyScript, 'check "Filter names"')).Count | Should -Be 1
+ $verifyScript | Should -Not -Match 'EXP_FILTERS'
+ $verifyScript | Should -Not -Match 'automations/incident-filters'
+ }
+
+ It 'requires every scheduled task to apply the shared output style' {
+ $taskFiles = @(Get-ChildItem -Path $script:ScheduledTaskDir -Filter '*.yaml')
+ $taskFiles.Count | Should -Be 19
+
+ $expectedInstruction = [regex]::Escape('Output style: Apply `ftk-output-style.md`')
+ foreach ($file in $taskFiles) {
+ $content = Get-Content -Path $file.FullName -Raw
+ $content | Should -Match $expectedInstruction
+ }
+ }
+
+ It 'deletes existing scheduled tasks before applying manifests' {
+ $applyExtrasScript = Get-Content -Path $script:ApplyExtrasScript -Raw
+ $verifyScript = Get-Content -Path (Join-Path $script:RepoRoot 'src/templates/sre-agent/bin/verify-agent.sh') -Raw
+
+ $applyExtrasScript | Should -Match 'delete_existing_scheduled_tasks'
+ $applyExtrasScript | Should -Match '/api/v1/scheduledtasks'
+ $applyExtrasScript | Should -Match 'dataplane_put_extended "scheduledtasks"'
+ $verifyScript | Should -Match 'Scheduled task duplicates'
+ $verifyScript | Should -Match '0 duplicates'
+ }
+
+ }
+
+ Context 'copy-and-update deployment contract' {
+ It 'rejects what-if because the copied starter-lab flow deploys directly' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "bash '$script:DeployScript' --recipe '$script:RecipeDir' --subscription 00000000-0000-0000-0000-000000000000 -g rg-test -n test-agent -l westus3 --what-if"
+ $result.ExitCode | Should -Be 2
+ $result.Output | Should -Match 'unknown flag'
+ }
+
+ It 'documents that azd is not used' {
+ if ($script:SkipBash) { Set-ItResult -Skipped -Because 'bash is unavailable' }
+ $result = Invoke-BashCommand "git grep -nF 'azd' -- src/templates/sre-agent/bin src/templates/sre-agent/infra docs-mslearn/toolkit/sre-agent/deploy.md src/templates/sre-agent/README.md"
+ $result.Output | Should -Match 'azd'
+ $result.Output | Should -Match 'not used|doesn''t use'
+ }
+ }
+}
diff --git a/src/scripts/Build-SreAgentTemplate.ps1 b/src/scripts/Build-SreAgentTemplate.ps1
new file mode 100644
index 000000000..8c2c2a5e2
--- /dev/null
+++ b/src/scripts/Build-SreAgentTemplate.ps1
@@ -0,0 +1,47 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+ .SYNOPSIS
+ Builds the SRE Agent recipe package used by the portal deployment template.
+
+ .PARAMETER DestDir
+ Release directory for the copied sre-agent template.
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)][string]$DestDir
+)
+
+$templateRoot = Resolve-Path "$PSScriptRoot/../templates/sre-agent"
+$recipeDir = Join-Path $templateRoot "recipes/finops-hub"
+$builder = Join-Path $templateRoot "bin/build-extras.py"
+$assetsDir = Join-Path $DestDir "assets"
+$workDir = Join-Path $assetsDir "sre-agent-recipe"
+$extrasPath = Join-Path $workDir "extras.json"
+$packagePath = Join-Path $assetsDir "sre-agent-recipe.zip"
+$placeholderKustoUri = "https://placeholder.eastus2.kusto.windows.net/Hub"
+
+if (-not (Get-Command python3 -ErrorAction SilentlyContinue))
+{
+ throw "python3 is required to build the SRE Agent recipe package."
+}
+
+& "$PSScriptRoot/New-Directory.ps1" $assetsDir
+Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue
+& "$PSScriptRoot/New-Directory.ps1" $workDir
+
+Write-Host " Building SRE Agent recipe package..."
+$summary = python3 $builder --recipe $recipeDir --output $extrasPath --kusto-connector-uri $placeholderKustoUri
+if (-not $?)
+{
+ throw "Failed to build SRE Agent extras manifest."
+}
+
+Write-Verbose " SRE Agent extras summary: $summary"
+Remove-Item $packagePath -Force -ErrorAction SilentlyContinue
+Compress-Archive -Path "$workDir/*" -DestinationPath $packagePath -Force
+Remove-Item $workDir -Recurse -Force
+
+Write-Host " Created assets/sre-agent-recipe.zip"
diff --git a/src/scripts/Build-Toolkit.ps1 b/src/scripts/Build-Toolkit.ps1
index cfa992eb6..e447e41a5 100644
--- a/src/scripts/Build-Toolkit.ps1
+++ b/src/scripts/Build-Toolkit.ps1
@@ -193,7 +193,7 @@ $templates | ForEach-Object {
# Create target directory
$destDir = "$outdir/$templateName"
Write-Verbose " Creating target directory: $destDir"
- Remove-Item $destDir -Recurse -ErrorAction SilentlyContinue
+ Remove-Item $destDir -Recurse -Force -ErrorAction SilentlyContinue
& "$PSScriptRoot/New-Directory.ps1" $destDir
# Copy required files
diff --git a/src/templates/sre-agent/.build.config b/src/templates/sre-agent/.build.config
new file mode 100644
index 000000000..ac067ed9b
--- /dev/null
+++ b/src/templates/sre-agent/.build.config
@@ -0,0 +1,9 @@
+{
+ "scripts": [
+ "Build-SreAgentTemplate.ps1"
+ ],
+ "ignore": [
+ "bin/__pycache__",
+ "submodules"
+ ]
+}
diff --git a/src/templates/sre-agent/README.md b/src/templates/sre-agent/README.md
index 82b0dffb8..00bc6a6e5 100644
--- a/src/templates/sre-agent/README.md
+++ b/src/templates/sre-agent/README.md
@@ -110,8 +110,7 @@ bash bin/deploy.sh \
[--cluster-resource-id /subscriptions/.../providers/Microsoft.Kusto/clusters/] \
[--target-resource-group ...] \
[--dry-run] \
- [--force] \
- [--no-telemetry]
+ [--force]
```
`bin/deploy.sh --help` is the CLI contract:
@@ -132,10 +131,10 @@ Optional:
Example: https://..kusto.windows.net/Hub
--cluster-resource-id Optional Kusto cluster ARM resource ID. Real deployments resolve this from --cluster-uri when possible; dry-run requires it.
--no-subscription-reader Do not assign Reader at subscription scope. Default: assign Reader.
+ --deployer-principal-type Principal type of the deployer (User or ServicePrincipal). Default: User.
--deploy-name Deployment name override. Defaults to a deterministic name.
--dry-run Validate inputs and write parameters without Azure calls.
--force Accepted for compatibility.
- --no-telemetry Accepted for compatibility.
-h, --help Show this help.
```
@@ -209,14 +208,13 @@ The GitHub Actions example passes the cluster URI as a script flag while keeping
## Migrating from env-var-driven deploys
-The old deploy path accepted config through environment variables such as `FINOPS_HUB_CLUSTER_URI`, `FINOPS_HUB_CLUSTER_RESOURCE_ID`, `SRE_AGENT_NO_TELEMETRY`, and `connectors.secrets.env`. Those inputs are no longer supported for config or identity.
+The old deploy path accepted config through environment variables such as `FINOPS_HUB_CLUSTER_URI`, `FINOPS_HUB_CLUSTER_RESOURCE_ID`, and `connectors.secrets.env`. Those inputs are no longer supported for config or identity.
Before:
```bash
export FINOPS_HUB_CLUSTER_URI="https://..kusto.windows.net/hub"
export FINOPS_HUB_CLUSTER_RESOURCE_ID="/subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/"
-export SRE_AGENT_NO_TELEMETRY=1
bash bin/deploy.sh recipes/finops-hub
```
@@ -230,8 +228,7 @@ bash bin/deploy.sh \
--name \
--location \
--cluster-uri https://..kusto.windows.net/Hub \
- --cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/ \
- --no-telemetry
+ --cluster-resource-id /subscriptions//resourceGroups//providers/Microsoft.Kusto/clusters/
```
The only supported environment-variable inputs are secrets:
diff --git a/src/templates/sre-agent/bin/apply-extras.sh b/src/templates/sre-agent/bin/apply-extras.sh
new file mode 100755
index 000000000..c90365b0c
--- /dev/null
+++ b/src/templates/sre-agent/bin/apply-extras.sh
@@ -0,0 +1,581 @@
+#!/usr/bin/env bash
+# =============================================================================
+# apply-extras.sh - Apply non-Bicep SRE Agent recipe assets
+#
+# Follows the microsoft/sre-agent template pattern: Bicep deploys the ARM
+# resource, then this script applies connectors, KnowledgeFile sources, skills,
+# subagents, tools, and scheduled tasks through the supported ARM/data-plane
+# surfaces.
+# =============================================================================
+
+set -euo pipefail
+
+usage() {
+ cat < --subscription --resource-group --name --recipe --build-dir [options]
+
+Required:
+ --endpoint SRE Agent endpoint
+ --subscription Azure subscription that contains the SRE Agent
+ --resource-group Resource group that contains the SRE Agent
+ --name SRE Agent name
+ --recipe Recipe directory
+ --build-dir Working directory for generated extras and request files
+
+Optional:
+ --kusto-connector-uri Database-qualified Kusto connector URI
+ --dry-run Build extras and request payloads without Azure calls
+ -h, --help Show this help
+EOF
+ exit "${1:-0}"
+}
+
+fail() {
+ echo "$1" >&2
+ exit "${2:-1}"
+}
+
+require_value() {
+ local flag="$1"
+ local value="${2:-}"
+ if [[ -z "$value" || "$value" == -* ]]; then
+ fail "Error: flag ${flag} requires a value" 2
+ fi
+}
+
+safe_name() {
+ printf '%s' "$1" | tr '/:' '__' | tr -cd 'A-Za-z0-9._-'
+}
+
+urlencode() {
+ printf '%s' "$1" | jq -sRr @uri
+}
+
+knowledge_source_name() {
+ basename "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-//; s/-$//'
+}
+
+get_sre_token() {
+ az account get-access-token --resource https://azuresre.dev --query accessToken -o tsv 2>/dev/null
+}
+
+ENDPOINT=""
+SUBSCRIPTION_ID=""
+RESOURCE_GROUP=""
+AGENT_NAME=""
+RECIPE_DIR=""
+BUILD_DIR=""
+KUSTO_CONNECTOR_URI=""
+DRY_RUN=""
+ARM_API_VERSION="2025-05-01-preview"
+REQUEST_DELAY_SECONDS="${SRE_AGENT_APPLY_REQUEST_DELAY_SECONDS:-15}"
+RETRY_DELAY_SECONDS="${SRE_AGENT_APPLY_RETRY_DELAY_SECONDS:-15}"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --endpoint)
+ require_value "--endpoint" "${2:-}"
+ ENDPOINT="$2"
+ shift 2
+ ;;
+ --subscription)
+ require_value "--subscription" "${2:-}"
+ SUBSCRIPTION_ID="$2"
+ shift 2
+ ;;
+ --resource-group)
+ require_value "--resource-group" "${2:-}"
+ RESOURCE_GROUP="$2"
+ shift 2
+ ;;
+ --name)
+ require_value "--name" "${2:-}"
+ AGENT_NAME="$2"
+ shift 2
+ ;;
+ --recipe)
+ require_value "--recipe" "${2:-}"
+ RECIPE_DIR="$2"
+ shift 2
+ ;;
+ --build-dir)
+ require_value "--build-dir" "${2:-}"
+ BUILD_DIR="$2"
+ shift 2
+ ;;
+ --kusto-connector-uri)
+ require_value "--kusto-connector-uri" "${2:-}"
+ KUSTO_CONNECTOR_URI="$2"
+ shift 2
+ ;;
+ --dry-run)
+ DRY_RUN="true"
+ shift
+ ;;
+ -h|--help)
+ usage 0
+ ;;
+ *)
+ fail "Error: unknown argument '$1'" 2
+ ;;
+ esac
+done
+
+[[ -n "$ENDPOINT" ]] || fail "Error: --endpoint is required" 2
+[[ -n "$SUBSCRIPTION_ID" ]] || fail "Error: --subscription is required" 2
+[[ -n "$RESOURCE_GROUP" ]] || fail "Error: --resource-group is required" 2
+[[ -n "$AGENT_NAME" ]] || fail "Error: --name is required" 2
+[[ -n "$RECIPE_DIR" ]] || fail "Error: --recipe is required" 2
+[[ -n "$BUILD_DIR" ]] || fail "Error: --build-dir is required" 2
+[[ -d "$RECIPE_DIR" ]] || fail "Error: recipe directory not found: $RECIPE_DIR" 1
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+RECIPE_DIR="$(cd "$RECIPE_DIR" && pwd)"
+mkdir -p "$BUILD_DIR"
+BUILD_DIR="$(cd "$BUILD_DIR" && pwd)"
+EXTRAS_FILE="${BUILD_DIR}/extras.json"
+REQUEST_DIR="${BUILD_DIR}/requests"
+RESPONSE_DIR="${BUILD_DIR}/responses"
+ARM_BASE="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.App/agents/${AGENT_NAME}"
+
+command -v jq >/dev/null || fail "jq is required" 1
+command -v base64 >/dev/null || fail "base64 is required" 1
+
+PYTHON_CMD=""
+if command -v python3 >/dev/null; then
+ PYTHON_CMD="python3"
+elif command -v python >/dev/null && python --version 2>&1 | grep -q "Python 3"; then
+ PYTHON_CMD="python"
+else
+ fail "Python 3 is required" 1
+fi
+
+"$PYTHON_CMD" -c "import yaml" 2>/dev/null || fail "PyYAML is required to build extras. Install with: pip install pyyaml" 1
+
+mkdir -p "$REQUEST_DIR" "$RESPONSE_DIR"
+
+echo ""
+echo "============================================="
+echo " SRE Agent - Apply Extras"
+echo "============================================="
+echo ""
+echo "Agent endpoint: $ENDPOINT"
+echo "Recipe: $RECIPE_DIR"
+echo "Build dir: $BUILD_DIR"
+echo ""
+
+echo "Step 1/8: Building extras manifest..."
+SUMMARY_JSON="$("$PYTHON_CMD" "${SCRIPT_DIR}/build-extras.py" \
+ --recipe "$RECIPE_DIR" \
+ --output "$EXTRAS_FILE" \
+ --kusto-connector-uri "$KUSTO_CONNECTOR_URI")"
+echo " extras: $EXTRAS_FILE"
+echo " summary: $SUMMARY_JSON"
+echo ""
+
+if [[ -z "$DRY_RUN" ]]; then
+ command -v az >/dev/null || fail "az is required to apply extras" 1
+ command -v curl >/dev/null || fail "curl is required to apply extras" 1
+ az account set --subscription "$SUBSCRIPTION_ID" >/dev/null
+ DP_TOKEN="$(get_sre_token)"
+ [[ -n "$DP_TOKEN" ]] || fail "Error: failed to get Azure SRE Agent bearer token. Run: az login --scope https://azuresre.dev/.default" 1
+else
+ DP_TOKEN="dry-run"
+ echo "Dry run enabled. Azure control-plane and data-plane calls will be skipped."
+ echo ""
+fi
+
+arm_put_connector() {
+ local name="$1"
+ local body_json="$2"
+ local safe
+ local body_file
+ local response_file
+ local result
+
+ safe="$(safe_name "$name")"
+ body_file="${REQUEST_DIR}/arm-connectors/${safe}.json"
+ response_file="${RESPONSE_DIR}/arm-connectors/${safe}.json"
+ mkdir -p "$(dirname "$body_file")" "$(dirname "$response_file")"
+ printf '%s\n' "$body_json" > "$body_file"
+
+ if [[ -n "$DRY_RUN" ]]; then
+ echo " dry-run ARM PUT connectors/${name} -> $body_file"
+ return 0
+ fi
+
+ for attempt in 1 2 3 4 5; do
+ if result="$(az rest -m PUT \
+ --url "${ARM_BASE}/connectors/${name}?api-version=${ARM_API_VERSION}" \
+ --body "@${body_file}" \
+ --headers "Content-Type=application/json" \
+ -o json 2>&1)"; then
+ printf '%s\n' "$result" > "$response_file"
+ echo " ARM PUT connectors/${name}: ok"
+ return 0
+ fi
+
+ printf '%s\n' "$result" > "$response_file"
+ echo " ARM PUT connectors/${name}: attempt ${attempt}/5 failed"
+ [[ "$attempt" != "5" ]] && sleep "$RETRY_DELAY_SECONDS"
+ done
+
+ sed -n '1,120p' "$response_file" >&2 || true
+ fail "Failed to apply connector ${name}" 1
+}
+
+dataplane_put_extended() {
+ local kind="$1"
+ local name="$2"
+ local type="$3"
+ local tags_json="$4"
+ local props_json="$5"
+ local safe
+ local body_file
+ local response_file
+ local body
+ local http_code
+ local encoded_name
+
+ safe="$(safe_name "$name")"
+ body_file="${REQUEST_DIR}/dataplane-${kind}/${safe}.json"
+ response_file="${RESPONSE_DIR}/dataplane-${kind}/${safe}.json"
+ mkdir -p "$(dirname "$body_file")" "$(dirname "$response_file")"
+ body="$(jq -nc --arg name "$name" --arg type "$type" --argjson tags "$tags_json" --argjson properties "$props_json" \
+ '{name: $name, type: $type, tags: $tags, properties: $properties}')"
+ printf '%s\n' "$body" > "$body_file"
+
+ if [[ -n "$DRY_RUN" ]]; then
+ echo " dry-run PUT ${kind}/${name} -> $body_file"
+ return 0
+ fi
+
+ encoded_name="$(urlencode "$name")"
+ for attempt in 1 2 3 4 5; do
+ if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \
+ -X PUT "${ENDPOINT}/api/v2/extendedAgent/${kind}/${encoded_name}" \
+ -H "Authorization: Bearer ${DP_TOKEN}" \
+ -H "Content-Type: application/json" \
+ --data-binary "@${body_file}")"; then
+ :
+ else
+ http_code="000"
+ fi
+
+ case "$http_code" in
+ 200|201|202|204)
+ echo " PUT ${kind}/${name}: ok"
+ return 0
+ ;;
+ esac
+
+ echo " PUT ${kind}/${name}: attempt ${attempt}/5 returned HTTP ${http_code}"
+ [[ "$attempt" != "5" ]] && sleep "$RETRY_DELAY_SECONDS"
+ done
+
+ sed -n '1,120p' "$response_file" >&2 || true
+ fail "Failed to apply ${kind}/${name}" 1
+}
+
+apply_built_in_tools_config() {
+ local config_json
+ local override_count
+ local body_file="${REQUEST_DIR}/built-in-tools/configure.json"
+ local response_file="${RESPONSE_DIR}/built-in-tools/configure.json"
+ local http_code
+
+ config_json="$(jq -c '.builtInTools // {"overrides":[]}' "$EXTRAS_FILE")"
+ override_count="$(jq -r '.overrides | length' <<< "$config_json")"
+ [[ "$override_count" != "0" ]] || {
+ echo " built-in tools: no overrides"
+ return 0
+ }
+
+ mkdir -p "$(dirname "$body_file")" "$(dirname "$response_file")"
+ jq '{overrides: [.overrides[] | {name, enabled}]}' <<< "$config_json" > "$body_file"
+
+ if [[ -n "$DRY_RUN" ]]; then
+ echo " dry-run POST agent/tools/configure -> $body_file"
+ return 0
+ fi
+
+ for attempt in 1 2 3 4 5; do
+ if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \
+ -X POST "${ENDPOINT}/api/v2/agent/tools/configure" \
+ -H "Authorization: Bearer ${DP_TOKEN}" \
+ -H "Content-Type: application/json" \
+ --data-binary "@${body_file}")"; then
+ :
+ else
+ http_code="000"
+ fi
+
+ case "$http_code" in
+ 200|201|202|204)
+ echo " built-in tools configured: ${override_count} overrides"
+ return 0
+ ;;
+ esac
+
+ echo " built-in tools: attempt ${attempt}/5 returned HTTP ${http_code}"
+ [[ "$attempt" != "5" ]] && sleep "$RETRY_DELAY_SECONDS"
+ done
+
+ sed -n '1,120p' "$response_file" >&2 || true
+ fail "Failed to configure built-in tools" 1
+}
+
+delete_existing_scheduled_tasks() {
+ local count
+ local response_file="${RESPONSE_DIR}/scheduledtasks.existing.json"
+ local name
+ local ids
+ local id
+ local http_code
+ local deleted=0
+
+ count="$(jq '.scheduledTasks // [] | length' "$EXTRAS_FILE")"
+ [[ "$count" -gt 0 ]] || return 0
+
+ if [[ -n "$DRY_RUN" ]]; then
+ echo " dry-run scheduled-task cleanup: ${count} task name(s)"
+ return 0
+ fi
+
+ if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \
+ "${ENDPOINT}/api/v1/scheduledtasks" \
+ -H "Authorization: Bearer ${DP_TOKEN}")"; then
+ :
+ else
+ http_code="000"
+ fi
+ [[ "$http_code" == "200" ]] || fail "Failed to list existing scheduled tasks before apply (HTTP ${http_code})" 1
+
+ for i in $(seq 0 $((count - 1))); do
+ name="$(jq -r --argjson index "$i" '.scheduledTasks[$index].metadata.name' "$EXTRAS_FILE")"
+ ids="$(jq -r --arg name "$name" '.[]? | select(.name == $name) | .id // empty' "$response_file" 2>/dev/null || true)"
+ while IFS= read -r id; do
+ [[ -n "$id" ]] || continue
+ if curl -sS -o /dev/null -X DELETE "${ENDPOINT}/api/v1/scheduledtasks/${id}" -H "Authorization: Bearer ${DP_TOKEN}"; then
+ echo " deleted existing scheduled-task ${name} (${id})"
+ deleted=$((deleted + 1))
+ else
+ fail "Failed to delete existing scheduled-task ${name} (${id})" 1
+ fi
+ done <<< "$ids"
+ done
+
+ [[ "$deleted" -eq 0 ]] || echo " scheduled-task cleanup: ${deleted} existing deleted"
+}
+
+verify_knowledge_docs() {
+ local expected_docs=("$@")
+ local response_file="${RESPONSE_DIR}/knowledge-sources/list.json"
+ local detail_file
+ local doc
+ local source_name
+ local entry
+ local indexed
+ local reason
+ local missing
+ local unindexed
+ local http_code
+
+ [[ "${#expected_docs[@]}" -gt 0 ]] || return 0
+ [[ -z "$DRY_RUN" ]] || {
+ echo " dry-run knowledge verification skipped"
+ return 0
+ }
+
+ mkdir -p "$(dirname "$response_file")"
+ echo " waiting for knowledge sources to index..."
+ for attempt in $(seq 1 20); do
+ if http_code="$(curl -sS -o "$response_file" -w "%{http_code}" \
+ "${ENDPOINT}/api/v2/extendedAgent/connectors" \
+ -H "Authorization: Bearer ${DP_TOKEN}")"; then
+ :
+ else
+ http_code="000"
+ fi
+
+ missing=0
+ unindexed=0
+ if [[ "$http_code" == "200" ]]; then
+ for doc in "${expected_docs[@]}"; do
+ source_name="$(knowledge_source_name "$doc")"
+ entry="$(jq -c --arg name "$source_name" '[.value[]? | select(.name == $name and .properties.dataConnectorType == "KnowledgeFile")][0] // empty' "$response_file" 2>/dev/null || true)"
+ if [[ -z "$entry" ]]; then
+ missing=$((missing + 1))
+ continue
+ fi
+
+ detail_file="${RESPONSE_DIR}/knowledge-sources/${source_name}.json"
+ if http_code="$(curl -sS -o "$detail_file" -w "%{http_code}" \
+ "${ENDPOINT}/api/v2/extendedAgent/connectors/${source_name}" \
+ -H "Authorization: Bearer ${DP_TOKEN}")"; then
+ :
+ else
+ http_code="000"
+ fi
+
+ indexed="$(jq -r '.properties.extendedProperties.createdAt // empty' "$detail_file" 2>/dev/null || true)"
+ [[ -n "$indexed" ]] || unindexed=$((unindexed + 1))
+ done
+
+ if [[ "$missing" -eq 0 && "$unindexed" -eq 0 ]]; then
+ echo " knowledge sources indexed: ${#expected_docs[@]} docs"
+ return 0
+ fi
+
+ echo " knowledge indexing attempt ${attempt}/20: ${missing} missing, ${unindexed} not indexed"
+ else
+ echo " knowledge indexing attempt ${attempt}/20: connectors returned HTTP ${http_code}"
+ fi
+
+ [[ "$attempt" -lt 20 ]] && sleep "$RETRY_DELAY_SECONDS"
+ done
+
+ echo " knowledge source status:" >&2
+ for doc in "${expected_docs[@]}"; do
+ source_name="$(knowledge_source_name "$doc")"
+ detail_file="${RESPONSE_DIR}/knowledge-sources/${source_name}.json"
+ entry="$(jq -c --arg name "$source_name" '[.value[]? | select(.name == $name and .properties.dataConnectorType == "KnowledgeFile")][0] // empty' "$response_file" 2>/dev/null || true)"
+ if [[ -z "$entry" ]]; then
+ echo " ${doc}: missing" >&2
+ else
+ indexed="$(jq -r '.properties.extendedProperties.createdAt // empty' "$detail_file" 2>/dev/null || true)"
+ reason="$(jq -r '.properties.extendedProperties.errorReason // ""' "$detail_file" 2>/dev/null || true)"
+ echo " ${doc}: indexed=${indexed}${reason:+ reason=${reason}}" >&2
+ fi
+ done
+ fail "Knowledge sources failed to index" 1
+}
+
+echo "Step 2/8: Applying connectors..."
+CONNECTOR_COUNT="$(jq '.connectors // [] | length' "$EXTRAS_FILE")"
+if [[ "$CONNECTOR_COUNT" -gt 0 ]]; then
+ for i in $(seq 0 $((CONNECTOR_COUNT - 1))); do
+ name="$(jq -r --argjson index "$i" '.connectors[$index].name' "$EXTRAS_FILE")"
+ connector_type="$(jq -r --argjson index "$i" '.connectors[$index].properties.dataConnectorType // .connectors[$index].properties.type // ""' "$EXTRAS_FILE")"
+ connector_type_lower="$(printf '%s' "$connector_type" | tr '[:upper:]' '[:lower:]')"
+ if [[ "$connector_type_lower" == "mcp" ]]; then
+ jq -e --argjson index "$i" '.connectors[$index].properties.extendedProperties.type == "http" and (.connectors[$index].properties.extendedProperties.endpoint // "") != ""' "$EXTRAS_FILE" >/dev/null \
+ || fail "Error: MCP connector ${name} must use properties.extendedProperties.type and endpoint" 1
+ jq -e --argjson index "$i" '(.connectors[$index].properties.extendedProperties.headers? | type) != "object"' "$EXTRAS_FILE" >/dev/null \
+ || fail "Error: MCP connector ${name} must flatten custom headers into properties.extendedProperties" 1
+ fi
+ body="$(jq -c --argjson index "$i" '{properties: (.connectors[$index].properties | if (.identity // "") == "" then . + {identity: "system"} else . end)}' "$EXTRAS_FILE")"
+ arm_put_connector "$name" "$body"
+ done
+else
+ echo " connectors: none"
+fi
+echo ""
+
+echo "Step 3/8: Configuring built-in tools..."
+apply_built_in_tools_config
+echo ""
+
+echo "Step 4/8: Uploading knowledge sources..."
+KNOWLEDGE_COUNT="$(jq '.knowledgeItems // [] | length' "$EXTRAS_FILE")"
+KNOWLEDGE_DOC_NAMES=()
+if [[ "$KNOWLEDGE_COUNT" -gt 0 ]]; then
+ for i in $(seq 0 $((KNOWLEDGE_COUNT - 1))); do
+ fname="$(jq -r --argjson index "$i" '.knowledgeItems[$index].name' "$EXTRAS_FILE")"
+ content_type="$(jq -r --argjson index "$i" '.knowledgeItems[$index].contentType // "application/octet-stream"' "$EXTRAS_FILE")"
+ source_name="$(knowledge_source_name "$fname")"
+ content_b64="$(jq -rj --argjson index "$i" '.knowledgeItems[$index].content' "$EXTRAS_FILE" | base64 | tr -d '\n')"
+ KNOWLEDGE_DOC_NAMES+=("$fname")
+ body="$(jq -nc \
+ --arg dataSource "$source_name" \
+ --arg displayName "$fname" \
+ --arg fileName "$fname" \
+ --arg fileContent "$content_b64" \
+ --arg contentType "$content_type" \
+ '{properties:{dataConnectorType:"KnowledgeFile",dataSource:$dataSource,extendedProperties:{displayName:$displayName,fileName:$fileName,fileContent:$fileContent,contentType:$contentType}}}')"
+ arm_put_connector "$source_name" "$body"
+ [[ -z "$DRY_RUN" && "$i" -lt $((KNOWLEDGE_COUNT - 1)) ]] && sleep "$REQUEST_DELAY_SECONDS"
+ done
+ verify_knowledge_docs "${KNOWLEDGE_DOC_NAMES[@]}"
+else
+ echo " knowledge sources: none"
+fi
+echo ""
+
+echo "Step 5/8: Applying tools..."
+TOOL_COUNT="$(jq '.tools // [] | length' "$EXTRAS_FILE")"
+if [[ "$TOOL_COUNT" -gt 0 ]]; then
+ for i in $(seq 0 $((TOOL_COUNT - 1))); do
+ name="$(jq -r --argjson index "$i" '.tools[$index].metadata.name' "$EXTRAS_FILE")"
+ props="$(jq -c --argjson index "$i" '
+ .tools[$index].spec
+ | if (.type // "") == "PythonTool" then .type = "PythonFunctionTool" else . end
+ ' "$EXTRAS_FILE")"
+ dataplane_put_extended "tools" "$name" "ExtendedAgentTool" "[]" "$props"
+ done
+else
+ echo " tools: none"
+fi
+echo ""
+
+echo "Step 6/8: Applying skills..."
+SKILL_COUNT="$(jq '.skills // [] | length' "$EXTRAS_FILE")"
+if [[ "$SKILL_COUNT" -gt 0 ]]; then
+ for i in $(seq 0 $((SKILL_COUNT - 1))); do
+ name="$(jq -r --argjson index "$i" '.skills[$index].metadata.name' "$EXTRAS_FILE")"
+ props="$(jq -c --argjson index "$i" '
+ {
+ name: .skills[$index].metadata.name,
+ description: (.skills[$index].metadata.description // ""),
+ tools: (.skills[$index].metadata.spec.tools // []),
+ skillContent: (.skills[$index].skillContent // ""),
+ additionalFiles: []
+ }' "$EXTRAS_FILE")"
+ dataplane_put_extended "skills" "$name" "Skill" "[]" "$props"
+ done
+else
+ echo " skills: none"
+fi
+echo ""
+
+echo "Step 7/8: Applying subagents..."
+SUBAGENT_COUNT="$(jq '.subagents // [] | length' "$EXTRAS_FILE")"
+if [[ "$SUBAGENT_COUNT" -gt 0 ]]; then
+ for i in $(seq 0 $((SUBAGENT_COUNT - 1))); do
+ name="$(jq -r --argjson index "$i" '.subagents[$index].metadata.name' "$EXTRAS_FILE")"
+ props="$(jq -c --argjson index "$i" '.subagents[$index].spec' "$EXTRAS_FILE")"
+ dataplane_put_extended "agents" "$name" "ExtendedAgent" "[]" "$props"
+ done
+else
+ echo " subagents: none"
+fi
+echo ""
+
+echo "Step 8/8: Applying scheduled tasks..."
+TASK_COUNT="$(jq '.scheduledTasks // [] | length' "$EXTRAS_FILE")"
+if [[ "$TASK_COUNT" -gt 0 ]]; then
+ delete_existing_scheduled_tasks
+ for i in $(seq 0 $((TASK_COUNT - 1))); do
+ name="$(jq -r --argjson index "$i" '.scheduledTasks[$index].metadata.name' "$EXTRAS_FILE")"
+ props="$(jq -c --argjson index "$i" '
+ .scheduledTasks[$index].spec as $spec |
+ {
+ name: ($spec.name // .scheduledTasks[$index].metadata.name // ""),
+ description: ($spec.description // ""),
+ cronExpression: ($spec.schedule // $spec.cronExpression // $spec.cron_expression // ""),
+ agentPrompt: ($spec.prompt // $spec.agentPrompt // $spec.agent_prompt // ""),
+ agentMode: ($spec.mode // $spec.agentMode // $spec.agent_mode // "Review"),
+ isEnabled: ($spec.enabled // true),
+ agent: ($spec.agent // "")
+ }' "$EXTRAS_FILE")"
+ dataplane_put_extended "scheduledtasks" "$name" "ScheduledTask" "[]" "$props"
+ done
+else
+ echo " scheduled tasks: none"
+fi
+echo ""
+
+echo "============================================="
+echo " SRE Agent extras complete"
+echo "============================================="
+echo ""
diff --git a/src/templates/sre-agent/bin/check-prerequisites.sh b/src/templates/sre-agent/bin/check-prerequisites.sh
new file mode 100755
index 000000000..2dd6b2566
--- /dev/null
+++ b/src/templates/sre-agent/bin/check-prerequisites.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+# check-prerequisites.sh — verify required tools are installed
+
+usage() {
+ cat <]
+
+Options:
+ --subscription Subscription to scope Azure CLI checks
+ -h, --help Show this help
+EOF
+ exit "${1:-0}"
+}
+
+check_prerequisites() {
+ local missing=0
+
+ for cmd in az curl git jq; do
+ if ! command -v "$cmd" &>/dev/null; then
+ echo " ❌ Missing: $cmd" >&2
+ missing=$((missing + 1))
+ fi
+ done
+
+ if ! command -v python3 &>/dev/null && ! command -v python &>/dev/null; then
+ echo " ❌ Missing: python3 (needed for YAML processing)" >&2
+ missing=$((missing + 1))
+ else
+ local py=$(command -v python3 || command -v python)
+ if ! "$py" -c "import yaml" 2>/dev/null; then
+ echo " ❌ Missing: PyYAML — install: pip install pyyaml" >&2
+ missing=$((missing + 1))
+ fi
+ fi
+
+ if [[ $missing -gt 0 ]]; then
+ echo " $missing prerequisite(s) missing. See README for install guide." >&2
+ return 1
+ fi
+ return 0
+}
+
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ SUBSCRIPTION_ID=""
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --subscription)
+ [[ -n "${2:-}" && "${2:-}" != -* ]] || { echo "Error: flag --subscription requires a value" >&2; exit 2; }
+ SUBSCRIPTION_ID="$2"
+ shift 2
+ ;;
+ -h|--help)
+ usage 0
+ ;;
+ *)
+ echo "Error: unexpected argument '$1'" >&2
+ usage 2
+ ;;
+ esac
+ done
+
+ if [[ -n "$SUBSCRIPTION_ID" ]]; then
+ export AZURE_SUBSCRIPTION_ID="$SUBSCRIPTION_ID"
+ fi
+
+ check_prerequisites
+fi
diff --git a/src/templates/sre-agent/bin/deploy.sh b/src/templates/sre-agent/bin/deploy.sh
new file mode 100755
index 000000000..e40373733
--- /dev/null
+++ b/src/templates/sre-agent/bin/deploy.sh
@@ -0,0 +1,566 @@
+#!/usr/bin/env bash
+# =============================================================================
+# deploy.sh - FinOps Toolkit SRE Agent setup
+#
+# Copied from microsoft/sre-agent labs/starter-lab/scripts/setup.sh and updated
+# for this template:
+# - uses Azure CLI + Bicep directly, not azd
+# - deploys the FinOps SRE Agent infrastructure only, not the Grubify lab app
+# - applies non-Bicep recipe assets with apply-extras after ARM succeeds
+# =============================================================================
+
+set -euo pipefail
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+INFRA_DIR="${PROJECT_DIR}/infra"
+
+usage() {
+ cat < [options]
+
+Required:
+ --recipe Recipe directory
+ --subscription Azure subscription
+ -g, --resource-group Resource group for the agent
+ -n, --name Agent name
+ -l, --location Azure region
+
+Optional:
+ --target-resource-group Repeatable target resource group. The agent resource group is always included.
+ --cluster-uri Kusto connector URI, including database name.
+ Example: https://..kusto.windows.net/Hub
+ --cluster-resource-id Optional Kusto cluster ARM resource ID. Real deployments resolve this from --cluster-uri when possible; dry-run requires it.
+ --no-subscription-reader Do not assign Reader at subscription scope. Default: assign Reader.
+ --deployer-principal-type Principal type of the deployer (User or ServicePrincipal). Default: User.
+ --deploy-name Deployment name override. Defaults to a deterministic name.
+ --dry-run Validate inputs and write parameters without Azure calls.
+ --force Accepted for compatibility.
+ -h, --help Show this help.
+EOF
+ exit "${1:-0}"
+}
+
+fail() {
+ echo -e "${RED}$1${NC}" >&2
+ exit "${2:-1}"
+}
+
+require_value() {
+ local flag="$1"
+ local value="${2:-}"
+ if [[ -z "$value" || "$value" == -* ]]; then
+ fail "Error: flag ${flag} requires a value" 2
+ fi
+}
+
+recipe_value() {
+ local file="$1"
+ local path="$2"
+ jq -r "$path // empty | if . == null or . == \"null\" then \"\" else . end" "$file"
+}
+
+deterministic_deploy_name() {
+ "$PYTHON_CMD" - "$SUBSCRIPTION_ID" "$RESOURCE_GROUP" "$AGENT_NAME" <<'PY'
+import hashlib
+import re
+import sys
+
+subscription_id, resource_group, agent_name = sys.argv[1:4]
+resource_group_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
+seed = f"{subscription_id}|{resource_group_id}|{agent_name}".lower()
+slug = re.sub(r"[^a-z0-9-]+", "-", agent_name.lower()).strip("-") or "agent"
+digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:12]
+name = f"sre-agent-{slug}-{digest}"
+print(name[:64].rstrip("-"))
+PY
+}
+
+deployment_output_value() {
+ local file="$1"
+ local key="$2"
+ local default="${3:-}"
+ jq -r \
+ --arg key "$key" \
+ --arg default "$default" \
+ '(.properties.outputs // {})
+ | to_entries
+ | map(select((.key | ascii_upcase) == ($key | ascii_upcase)) | .value.value)
+ | first // $default' \
+ "$file"
+}
+
+normalize_action_mode() {
+ case "$1" in
+ Autonomous|autonomous|Automatic|automatic) printf 'autonomous' ;;
+ Review|review) printf 'review' ;;
+ ReadOnly|readOnly|readonly) printf 'readOnly' ;;
+ *) fail "Error: unsupported action mode '$1'" 2 ;;
+ esac
+}
+
+validate_kusto_uri() {
+ local uri="$1"
+ [[ -z "$uri" ]] && return 0
+ if [[ "$uri" != https://*.kusto.windows.net/* ]]; then
+ fail "Error: --cluster-uri must be a database-qualified Kusto URI. Example: https://..kusto.windows.net/Hub" 2
+ fi
+ local path="${uri#https://}"
+ if [[ "$path" != */* || -z "${path#*/}" ]]; then
+ fail "Error: --cluster-uri must include the Kusto database name. Example: https://..kusto.windows.net/Hub" 2
+ fi
+}
+
+parse_kusto_cluster_name() {
+ local uri="$1"
+ local host
+ host="${uri#https://}"
+ host="${host%%/*}"
+ printf '%s\n' "${host%%.*}"
+}
+
+parse_kusto_database_name() {
+ local uri="$1"
+ local path
+ path="${uri#https://}"
+ path="${path#*/}"
+ path="${path%%\?*}"
+ path="${path%%#*}"
+ printf '%s\n' "${path%%/*}"
+}
+
+to_lower() {
+ printf '%s\n' "$1" | tr '[:upper:]' '[:lower:]'
+}
+
+append_target_rg() {
+ local rg="$1"
+ local existing
+ [[ -n "$rg" ]] || return 0
+ if [[ "${#TARGET_RGS[@]}" -gt 0 ]]; then
+ for existing in "${TARGET_RGS[@]}"; do
+ if [[ "$(to_lower "$existing")" == "$(to_lower "$rg")" ]]; then
+ return 0
+ fi
+ done
+ fi
+ TARGET_RGS+=("$rg")
+}
+
+resource_id_subscription() {
+ local resource_id="$1"
+ printf '%s\n' "$resource_id" | awk -F/ '{print $3}'
+}
+
+resource_id_resource_group() {
+ local resource_id="$1"
+ printf '%s\n' "$resource_id" | awk -F/ '{print $5}'
+}
+
+resolve_kusto_cluster_resource_id() {
+ local uri="$1"
+ local cluster_name
+ local cluster_uri
+ local resource_id
+ local graph_result_count
+
+ cluster_name="$(parse_kusto_cluster_name "$uri")"
+ [[ -n "$cluster_name" ]] || fail "Error: could not parse Kusto cluster name from --cluster-uri" 2
+ cluster_uri="${uri%/*}"
+
+ resource_id="$(az resource list \
+ --subscription "$SUBSCRIPTION_ID" \
+ --resource-type Microsoft.Kusto/clusters \
+ --query "[?name=='${cluster_name}'].id | [0]" \
+ -o tsv 2>/dev/null || true)"
+
+ if [[ -n "$resource_id" && "$resource_id" != "null" ]]; then
+ printf '%s\n' "$resource_id"
+ return 0
+ fi
+
+ resource_id="$(az graph query \
+ -q "Resources | where type =~ 'microsoft.kusto/clusters' | where name =~ '${cluster_name}' or tostring(properties.uri) =~ '${cluster_uri}' | project id | limit 2" \
+ -o json 2>/dev/null | jq -r '.data[].id' 2>/dev/null || true)"
+ graph_result_count="$(printf '%s\n' "$resource_id" | sed '/^$/d' | wc -l | tr -d ' ')"
+
+ if [[ "$graph_result_count" == "1" ]]; then
+ printf '%s\n' "$resource_id"
+ return 0
+ fi
+
+ if [[ -z "$resource_id" || "$resource_id" == "null" ]]; then
+ fail "Error: could not resolve Kusto cluster '${cluster_name}'. Pass --cluster-resource-id so deployment can assign AllDatabasesViewer before creating the connector." 2
+ fi
+
+ fail "Error: Kusto cluster '${cluster_name}' resolved to multiple resources. Pass --cluster-resource-id explicitly so deployment assigns AllDatabasesViewer to the intended cluster." 2
+}
+
+warn_kusto_private_query_limitation() {
+ local cluster_id="$1"
+ local cluster_json
+ local public_network_access
+ local private_endpoint_count
+ local cluster_uri
+ local docs_url="https://sre.azure.com/docs/capabilities/azure-observability-vnet#known-limitations"
+
+ [[ -n "$cluster_id" ]] || return 0
+
+ cluster_json="$(az resource show --ids "$cluster_id" --api-version 2023-08-15 -o json 2>/dev/null || true)"
+ if [[ -z "$cluster_json" ]]; then
+ echo -e "${YELLOW}Warning: Could not inspect Kusto cluster network access. Deployment will continue.${NC}"
+ echo " Cluster: $cluster_id"
+ echo " Review SRE Agent private endpoint limitations: $docs_url"
+ echo ""
+ return 0
+ fi
+
+ public_network_access="$(echo "$cluster_json" | jq -r '.properties.publicNetworkAccess // .publicNetworkAccess // ""' 2>/dev/null || true)"
+ private_endpoint_count="$(echo "$cluster_json" | jq -r '((.properties.privateEndpointConnections // .privateEndpointConnections // []) | length)' 2>/dev/null || echo 0)"
+ cluster_uri="$(echo "$cluster_json" | jq -r '.properties.uri // .uri // empty' 2>/dev/null || true)"
+
+ if [[ "$public_network_access" == "Disabled" ]]; then
+ echo -e "${YELLOW}Warning: The Kusto cluster denies public query access.${NC}"
+ echo " Cluster: ${cluster_uri:-$cluster_id}"
+ echo " publicNetworkAccess: ${public_network_access}"
+ echo " private endpoint connections: ${private_endpoint_count}"
+ echo " SRE Agent will still be deployed and the finops-hub-kusto connector will still be created."
+ echo " Per the SRE Agent known limitations, private endpoint ADX blocks direct KQL queries."
+ echo " The customer can enable public query access if they want the Kusto connector to become healthy:"
+ echo " $docs_url"
+ echo ""
+ fi
+}
+
+RECIPE_DIR=""
+SUBSCRIPTION_ID=""
+RESOURCE_GROUP=""
+AGENT_NAME=""
+LOCATION=""
+CLUSTER_URI=""
+CLUSTER_RESOURCE_ID=""
+DEPLOY_NAME=""
+DRY_RUN=""
+ENABLE_SUBSCRIPTION_READER="false"
+DEPLOYER_PRINCIPAL_TYPE="User"
+TARGET_RGS=()
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --recipe)
+ require_value "--recipe" "${2:-}"
+ RECIPE_DIR="$2"
+ shift 2
+ ;;
+ --subscription)
+ require_value "--subscription" "${2:-}"
+ SUBSCRIPTION_ID="$2"
+ shift 2
+ ;;
+ -g|--resource-group)
+ require_value "--resource-group / -g" "${2:-}"
+ RESOURCE_GROUP="$2"
+ shift 2
+ ;;
+ -n|--name)
+ require_value "--name / -n" "${2:-}"
+ AGENT_NAME="$2"
+ shift 2
+ ;;
+ -l|--location)
+ require_value "--location / -l" "${2:-}"
+ LOCATION="$2"
+ shift 2
+ ;;
+ --target-resource-group)
+ require_value "--target-resource-group" "${2:-}"
+ TARGET_RGS+=("$2")
+ shift 2
+ ;;
+ --cluster-uri)
+ require_value "--cluster-uri" "${2:-}"
+ CLUSTER_URI="$2"
+ shift 2
+ ;;
+ --cluster-resource-id)
+ require_value "--cluster-resource-id" "${2:-}"
+ CLUSTER_RESOURCE_ID="$2"
+ shift 2
+ ;;
+ --subscription-reader)
+ ENABLE_SUBSCRIPTION_READER="true"
+ shift
+ ;;
+ --no-subscription-reader)
+ ENABLE_SUBSCRIPTION_READER="false"
+ shift
+ ;;
+ --deployer-principal-type)
+ require_value "--deployer-principal-type" "${2:-}"
+ DEPLOYER_PRINCIPAL_TYPE="$2"
+ shift 2
+ ;;
+ --deploy-name)
+ require_value "--deploy-name" "${2:-}"
+ DEPLOY_NAME="$2"
+ shift 2
+ ;;
+ --dry-run)
+ DRY_RUN="true"
+ shift
+ ;;
+ --force)
+ shift
+ ;;
+ -h|--help)
+ usage 0
+ ;;
+ -*)
+ fail "Error: unknown flag '$1'" 2
+ ;;
+ *)
+ fail "Error: unknown argument '$1'" 2
+ ;;
+ esac
+done
+
+[[ -n "$RECIPE_DIR" ]] || fail "Error: --recipe is required" 2
+[[ -d "$RECIPE_DIR" ]] || fail "Error: recipe directory not found: $RECIPE_DIR" 1
+[[ -f "${RECIPE_DIR}/agent.json" ]] || fail "Error: recipe agent.json not found: ${RECIPE_DIR}/agent.json" 1
+[[ -n "$SUBSCRIPTION_ID" ]] || fail "Error: --subscription is required" 2
+[[ -n "$RESOURCE_GROUP" ]] || fail "Error: --resource-group is required" 2
+[[ -n "$AGENT_NAME" ]] || fail "Error: --name is required" 2
+[[ -n "$LOCATION" ]] || fail "Error: --location is required" 2
+validate_kusto_uri "$CLUSTER_URI"
+if [[ -n "$CLUSTER_URI" ]]; then
+ [[ -n "$(parse_kusto_database_name "$CLUSTER_URI")" ]] || fail "Error: could not parse Kusto database name from --cluster-uri" 2
+fi
+
+append_target_rg "$RESOURCE_GROUP"
+
+command -v az >/dev/null || fail "Azure CLI (az) is required" 1
+command -v jq >/dev/null || fail "jq is required" 1
+command -v git >/dev/null || fail "git is required" 1
+
+PYTHON_CMD=""
+if command -v python3 >/dev/null; then
+ PYTHON_CMD="python3"
+elif command -v python >/dev/null && python --version 2>&1 | grep -q "Python 3"; then
+ PYTHON_CMD="python"
+else
+ fail "Python 3 is required" 1
+fi
+
+if [[ -n "$CLUSTER_URI" && -z "$CLUSTER_RESOURCE_ID" ]]; then
+ if [[ -n "$DRY_RUN" ]]; then
+ fail "Error: --cluster-resource-id is required with --cluster-uri for --dry-run because dry-run makes no Azure calls. Real deployments can resolve it from the Kusto URI." 2
+ fi
+
+ echo "Resolving Kusto cluster resource ID from --cluster-uri..."
+ az account show --subscription "$SUBSCRIPTION_ID" >/dev/null
+ az account set --subscription "$SUBSCRIPTION_ID"
+ CLUSTER_RESOURCE_ID="$(resolve_kusto_cluster_resource_id "$CLUSTER_URI")"
+ echo " Kusto cluster: $CLUSTER_RESOURCE_ID"
+fi
+
+if [[ -n "$CLUSTER_RESOURCE_ID" && -z "$DRY_RUN" ]]; then
+ warn_kusto_private_query_limitation "$CLUSTER_RESOURCE_ID"
+fi
+
+if [[ -n "$CLUSTER_RESOURCE_ID" ]]; then
+ CLUSTER_SUBSCRIPTION_ID="$(resource_id_subscription "$CLUSTER_RESOURCE_ID")"
+ CLUSTER_RESOURCE_GROUP="$(resource_id_resource_group "$CLUSTER_RESOURCE_ID")"
+ if [[ "$(to_lower "$CLUSTER_SUBSCRIPTION_ID")" == "$(to_lower "$SUBSCRIPTION_ID")" ]]; then
+ append_target_rg "$CLUSTER_RESOURCE_GROUP"
+ else
+ echo -e "${YELLOW}Warning: FinOps Hub cluster is in subscription ${CLUSTER_SUBSCRIPTION_ID}; add agent managed-resource scope and resource-group RBAC for ${CLUSTER_RESOURCE_GROUP} separately.${NC}" >&2
+ fi
+fi
+
+ACCESS_LEVEL="$(recipe_value "${RECIPE_DIR}/agent.json" '.access.accessLevel')"
+[[ -n "$ACCESS_LEVEL" ]] || ACCESS_LEVEL="Low"
+ACTION_MODE_RAW="$(recipe_value "${RECIPE_DIR}/agent.json" '.access.actionMode')"
+[[ -n "$ACTION_MODE_RAW" ]] || ACTION_MODE_RAW="Review"
+ACTION_MODE="$(normalize_action_mode "$ACTION_MODE_RAW")"
+UPGRADE_CHANNEL="$(recipe_value "${RECIPE_DIR}/agent.json" '.upgradeChannel')"
+[[ -n "$UPGRADE_CHANNEL" ]] || UPGRADE_CHANNEL="Preview"
+DEFAULT_MODEL_PROVIDER="$(recipe_value "${RECIPE_DIR}/agent.json" '.defaultModelProvider')"
+[[ -n "$DEFAULT_MODEL_PROVIDER" ]] || DEFAULT_MODEL_PROVIDER="MicrosoftFoundry"
+DEFAULT_MODEL_NAME="$(recipe_value "${RECIPE_DIR}/agent.json" '.defaultModelName')"
+[[ -n "$DEFAULT_MODEL_NAME" ]] || DEFAULT_MODEL_NAME="Automatic"
+MONTHLY_AGENT_UNIT_LIMIT="$(recipe_value "${RECIPE_DIR}/agent.json" '.monthlyAgentUnitLimit')"
+[[ -n "$MONTHLY_AGENT_UNIT_LIMIT" ]] || MONTHLY_AGENT_UNIT_LIMIT="10000"
+EXPERIMENTAL_SETTINGS="$(jq -c '.experimentalSettings // {"EnableSandboxGroup": true, "EnableWorkspaceTools": true}' "${RECIPE_DIR}/agent.json")"
+TAGS="$(jq -c '.tags // {"finops-toolkit":"sre-agent","source":"microsoft-finops-toolkit"}' "${RECIPE_DIR}/agent.json")"
+TARGET_RGS_JSON="$(printf '%s\n' "${TARGET_RGS[@]}" | jq -R . | jq -sc '.')"
+
+[[ -n "$DEPLOY_NAME" ]] || DEPLOY_NAME="$(deterministic_deploy_name)"
+BUILD_ROOT="${SRE_AGENT_DEPLOY_DIR:-${HOME}/.cache/finops-toolkit/sre-agent}"
+BUILD_DIR="${BUILD_ROOT}/${AGENT_NAME}-${DEPLOY_NAME}"
+mkdir -p "$BUILD_DIR"
+
+PARAMETERS_FILE="${BUILD_DIR}/deploy.parameters.json"
+RESULT_FILE="${BUILD_DIR}/deployment-result.json"
+
+jq -n \
+ --arg resourceGroupName "$RESOURCE_GROUP" \
+ --arg agentName "$AGENT_NAME" \
+ --arg location "$LOCATION" \
+ --arg accessLevel "$ACCESS_LEVEL" \
+ --arg actionMode "$ACTION_MODE" \
+ --arg upgradeChannel "$UPGRADE_CHANNEL" \
+ --arg defaultModelProvider "$DEFAULT_MODEL_PROVIDER" \
+ --arg defaultModelName "$DEFAULT_MODEL_NAME" \
+ --argjson monthlyAgentUnitLimit "$MONTHLY_AGENT_UNIT_LIMIT" \
+ --arg kustoClusterId "$CLUSTER_RESOURCE_ID" \
+ --argjson enableSubscriptionReaderRole "$ENABLE_SUBSCRIPTION_READER" \
+ --arg deployerPrincipalType "$DEPLOYER_PRINCIPAL_TYPE" \
+ --argjson targetResourceGroups "$TARGET_RGS_JSON" \
+ --argjson experimentalSettings "$EXPERIMENTAL_SETTINGS" \
+ --argjson tags "$TAGS" \
+ '{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "resourceGroupName": { "value": $resourceGroupName },
+ "agentName": { "value": $agentName },
+ "location": { "value": $location },
+ "targetResourceGroups": { "value": $targetResourceGroups },
+ "accessLevel": { "value": $accessLevel },
+ "actionMode": { "value": $actionMode },
+ "upgradeChannel": { "value": $upgradeChannel },
+ "defaultModelProvider": { "value": $defaultModelProvider },
+ "defaultModelName": { "value": $defaultModelName },
+ "monthlyAgentUnitLimit": { "value": $monthlyAgentUnitLimit },
+ "experimentalSettings": { "value": $experimentalSettings },
+ "tags": { "value": $tags },
+ "finopsHubKustoClusterResourceId": { "value": $kustoClusterId },
+ "enableSubscriptionReaderRole": { "value": $enableSubscriptionReaderRole },
+ "deployerPrincipalType": { "value": $deployerPrincipalType }
+ }
+ }' > "$PARAMETERS_FILE"
+
+echo ""
+echo -e "${BLUE}============================================================${NC}"
+echo -e "${BLUE} Azure SRE Agent - FinOps Toolkit Setup${NC}"
+echo -e "${BLUE}============================================================${NC}"
+echo ""
+echo -e "${YELLOW}[1/4] Checking prerequisites...${NC}"
+echo " az: $(az version --query '\"azure-cli\"' -o tsv 2>/dev/null || echo found)"
+echo " jq: $(jq --version)"
+echo " python: $($PYTHON_CMD --version 2>&1)"
+echo " azd: not used"
+echo ""
+
+if [[ -n "$DRY_RUN" ]]; then
+ echo -e "${YELLOW}[2/4] Planned deployment...${NC}"
+ echo " Subscription: $SUBSCRIPTION_ID"
+ echo " Resource group: $RESOURCE_GROUP"
+ echo " Agent: $AGENT_NAME"
+ echo " Region: $LOCATION"
+ echo " Target resource groups: ${TARGET_RGS[*]}"
+ echo " Subscription Reader: $ENABLE_SUBSCRIPTION_READER"
+ echo " Parameters: $PARAMETERS_FILE"
+ echo ""
+ echo "Dry run complete. No Azure calls were made."
+ exit 0
+fi
+
+echo -e "${YELLOW}[2/4] Checking Azure account...${NC}"
+az account show --subscription "$SUBSCRIPTION_ID" --query "{subscription:name, id:id}" -o table >/dev/null
+az account set --subscription "$SUBSCRIPTION_ID"
+echo " Subscription: $SUBSCRIPTION_ID"
+echo " Resource group: $RESOURCE_GROUP"
+echo " Agent: $AGENT_NAME"
+echo " Region: $LOCATION"
+echo " Target resource groups: ${TARGET_RGS[*]}"
+echo " Subscription Reader: $ENABLE_SUBSCRIPTION_READER"
+echo " Parameters: $PARAMETERS_FILE"
+echo ""
+
+echo -e "${YELLOW}[3/4] Deploying infrastructure with Azure CLI + Bicep...${NC}"
+echo " Registering Microsoft.App provider..."
+az provider register -n Microsoft.App --wait --output none
+
+echo " Starting deployment: $DEPLOY_NAME"
+az deployment sub create \
+ --subscription "$SUBSCRIPTION_ID" \
+ --location "$LOCATION" \
+ --name "$DEPLOY_NAME" \
+ --template-file "${INFRA_DIR}/main.bicep" \
+ --parameters "@${PARAMETERS_FILE}" \
+ --no-wait \
+ --output none
+
+echo " Waiting for deployment to complete..."
+DEPLOYMENT_START="$(date +%s)"
+DEPLOYMENT_JSON="{}"
+STATE=""
+while true; do
+ DEPLOYMENT_JSON="$(az deployment sub show \
+ --subscription "$SUBSCRIPTION_ID" \
+ --name "$DEPLOY_NAME" \
+ --output json 2>/dev/null || echo "{}")"
+ STATE="$(echo "$DEPLOYMENT_JSON" | jq -r '.properties.provisioningState // "Accepted"' 2>/dev/null)"
+
+ case "$STATE" in
+ Succeeded|Failed|Canceled)
+ break
+ ;;
+ *)
+ NOW="$(date +%s)"
+ echo " Deployment state: ${STATE} ($((NOW - DEPLOYMENT_START))s elapsed)"
+ sleep 10
+ ;;
+ esac
+done
+
+printf '%s\n' "$DEPLOYMENT_JSON" > "$RESULT_FILE"
+
+STATE="$(jq -r '.properties.provisioningState // "Failed"' "$RESULT_FILE")"
+if [[ "$STATE" != "Succeeded" ]]; then
+ echo ""
+ echo "Deployment failed. Diagnostic command:"
+ echo " az deployment operation sub list --subscription ${SUBSCRIPTION_ID} -n ${DEPLOY_NAME} -o table"
+ exit 1
+fi
+
+AGENT_ENDPOINT="$(deployment_output_value "$RESULT_FILE" "SRE_AGENT_ENDPOINT")"
+SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID="$(deployment_output_value "$RESULT_FILE" "SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID")"
+AGENT_PORTAL_URL="$(deployment_output_value "$RESULT_FILE" "AGENT_PORTAL_URL" "https://sre.azure.com")"
+
+[[ -n "$AGENT_ENDPOINT" ]] || fail "Deployment succeeded but did not return SRE_AGENT_ENDPOINT" 1
+[[ -n "$SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID" ]] || fail "Deployment succeeded but did not return SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID" 1
+
+echo ""
+echo -e "${YELLOW}[4/4] Applying SRE Agent extras...${NC}"
+APPLY_EXTRAS_ARGS=(
+ --endpoint "$AGENT_ENDPOINT"
+ --subscription "$SUBSCRIPTION_ID"
+ --resource-group "$RESOURCE_GROUP"
+ --name "$AGENT_NAME"
+ --recipe "$RECIPE_DIR"
+ --build-dir "${BUILD_DIR}/extras"
+)
+
+if [[ -n "$CLUSTER_URI" ]]; then
+ APPLY_EXTRAS_ARGS+=(--kusto-connector-uri "$CLUSTER_URI")
+fi
+
+bash "${SCRIPT_DIR}/apply-extras.sh" \
+ "${APPLY_EXTRAS_ARGS[@]}"
+
+echo ""
+echo -e "${BLUE}============================================================${NC}"
+echo -e "${GREEN} SRE Agent ready${NC}"
+echo -e "${BLUE}============================================================${NC}"
+echo " Agent portal: $AGENT_PORTAL_URL"
+echo " Endpoint: $AGENT_ENDPOINT"
+echo " Build dir: $BUILD_DIR"
+echo ""
diff --git a/src/templates/sre-agent/bin/post-provision.sh b/src/templates/sre-agent/bin/post-provision.sh
new file mode 100755
index 000000000..4bf44af9f
--- /dev/null
+++ b/src/templates/sre-agent/bin/post-provision.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Compatibility wrapper. The SRE Agent recipe now follows the upstream
+# Bicep + apply-extras pattern for post-provisioning.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+echo "bin/post-provision.sh is deprecated; forwarding to bin/apply-extras.sh" >&2
+exec bash "${SCRIPT_DIR}/apply-extras.sh" "$@"
diff --git a/src/templates/sre-agent/createUiDefinition.json b/src/templates/sre-agent/createUiDefinition.json
new file mode 100644
index 000000000..25d538b3e
--- /dev/null
+++ b/src/templates/sre-agent/createUiDefinition.json
@@ -0,0 +1,169 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#",
+ "handler": "Microsoft.Azure.CreateUIDef",
+ "version": "0.1.2-preview",
+ "parameters": {
+ "config": {
+ "basics": {
+ "description": "Deploy Azure SRE Agent with the FinOps toolkit recipe for FinOps hub analysis, capacity monitoring, scheduled tasks, and specialist agents.",
+ "location": {
+ "label": "Location",
+ "resourceTypes": [
+ "Microsoft.App/agents",
+ "Microsoft.Insights/components",
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "Microsoft.OperationalInsights/workspaces",
+ "Microsoft.Resources/deploymentScripts"
+ ]
+ }
+ }
+ },
+ "resourceTypes": [
+ "Microsoft.App/agents",
+ "Microsoft.Insights/components",
+ "Microsoft.ManagedIdentity/userAssignedIdentities",
+ "Microsoft.OperationalInsights/workspaces",
+ "Microsoft.Resources/deploymentScripts"
+ ],
+ "basics": [
+ {
+ "name": "resourceGroupName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent resource group",
+ "defaultValue": "finops-hub-sre",
+ "toolTip": "Resource group that will contain the Azure SRE Agent, monitoring resources, and deployment script identity.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-zA-Z0-9._()\\-]{1,90}$",
+ "validationMessage": "Enter a valid Azure resource group name."
+ },
+ "visible": true
+ },
+ {
+ "name": "agentName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent name",
+ "defaultValue": "finops-hub-sre",
+ "toolTip": "Name of the Azure SRE Agent resource.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,61}[a-zA-Z0-9]$",
+ "validationMessage": "Name must be 3-63 characters and can contain letters, numbers, and hyphens. The first and last characters must be alphanumeric."
+ },
+ "visible": true
+ }
+ ],
+ "steps": [
+ {
+ "name": "configuration",
+ "label": "Configuration",
+ "elements": [
+ {
+ "name": "finopsHubKustoConnectorUri",
+ "type": "Microsoft.Common.TextBox",
+ "label": "FinOps hub Kusto URI",
+ "defaultValue": "",
+ "toolTip": "Optional database-qualified Azure Data Explorer URI for the FinOps hub. Example: https://cluster.region.kusto.windows.net/Hub",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^https://.+\\.kusto\\.windows\\.net/.+",
+ "validationMessage": "Enter a database-qualified Kusto URI such as https://cluster.region.kusto.windows.net/Hub, or leave blank."
+ },
+ "visible": true
+ },
+ {
+ "name": "finopsHubKustoClusterResourceId",
+ "type": "Microsoft.Common.TextBox",
+ "label": "FinOps hub Kusto cluster resource ID",
+ "defaultValue": "",
+ "toolTip": "Optional Azure Data Explorer cluster resource ID. Provide this to assign the agent managed identity AllDatabasesViewer on the cluster.",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft\\.Kusto/clusters/.+",
+ "validationMessage": "Enter a valid Microsoft.Kusto/clusters resource ID, or leave blank."
+ },
+ "visible": true
+ },
+ {
+ "name": "targetResourceGroups",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Additional target resource groups",
+ "defaultValue": "",
+ "toolTip": "Optional comma-separated resource group names the agent can observe or act on. The agent resource group is always included.",
+ "constraints": {
+ "required": false,
+ "regex": "^$|^[A-Za-z0-9_.()\\-]+(\\s*,\\s*[A-Za-z0-9_.()\\-]+)*$",
+ "validationMessage": "Enter a comma-separated list of resource group names without blank entries or trailing commas."
+ },
+ "visible": true
+ },
+ {
+ "name": "accessLevel",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Access level",
+ "defaultValue": "Low",
+ "toolTip": "Low grants read-only monitoring roles (recommended for reporting and analysis). High adds Contributor on target resource groups for autonomous remediation workflows.",
+ "constraints": {
+ "allowedValues": [
+ {
+ "label": "High",
+ "value": "High"
+ },
+ {
+ "label": "Low",
+ "value": "Low"
+ }
+ ],
+ "required": true
+ },
+ "visible": true
+ },
+ {
+ "name": "actionMode",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Action mode",
+ "defaultValue": "autonomous",
+ "toolTip": "Review requires approval before write actions. Autonomous lets the agent run enabled actions within its assigned access.",
+ "constraints": {
+ "allowedValues": [
+ {
+ "label": "Autonomous",
+ "value": "autonomous"
+ },
+ {
+ "label": "Review",
+ "value": "review"
+ },
+ {
+ "label": "Read only",
+ "value": "readOnly"
+ }
+ ],
+ "required": true
+ },
+ "visible": true
+ },
+ {
+ "name": "enableSubscriptionReaderRole",
+ "type": "Microsoft.Common.CheckBox",
+ "label": "Grant subscription Reader to the agent",
+ "toolTip": "Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.",
+ "defaultValue": false,
+ "visible": true
+ }
+ ]
+ }
+ ],
+ "outputs": {
+ "resourceGroupName": "[basics('resourceGroupName')]",
+ "agentName": "[basics('agentName')]",
+ "location": "[location()]",
+ "targetResourceGroupNames": "[steps('configuration').targetResourceGroups]",
+ "finopsHubKustoConnectorUri": "[steps('configuration').finopsHubKustoConnectorUri]",
+ "finopsHubKustoClusterResourceId": "[steps('configuration').finopsHubKustoClusterResourceId]",
+ "accessLevel": "[steps('configuration').accessLevel]",
+ "actionMode": "[steps('configuration').actionMode]",
+ "enableSubscriptionReaderRole": "[steps('configuration').enableSubscriptionReaderRole]"
+ }
+ }
+}
diff --git a/src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml b/src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml
new file mode 100644
index 000000000..a13358445
--- /dev/null
+++ b/src/templates/sre-agent/examples/ci-cd/github-actions-deploy.yml
@@ -0,0 +1,33 @@
+name: Deploy FinOps SRE Agent
+
+on:
+ workflow_dispatch:
+
+permissions:
+ id-token: write
+ contents: read
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ - name: Deploy FinOps SRE Agent
+ working-directory: src/templates/sre-agent
+ env:
+ CLUSTER_URI: ${{ secrets.SRE_AGENT_CLUSTER_URI }}
+ run: >
+ bash bin/deploy.sh
+ --recipe recipes/finops-hub
+ --resource-group ${{ vars.SRE_AGENT_RESOURCE_GROUP }}
+ --name ${{ vars.SRE_AGENT_NAME }}
+ --location ${{ vars.SRE_AGENT_LOCATION }}
+ --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ --cluster-uri "$CLUSTER_URI"
+ --deployer-principal-type ServicePrincipal
+ --force
diff --git a/src/templates/sre-agent/infra/main.bicep b/src/templates/sre-agent/infra/main.bicep
new file mode 100644
index 000000000..67198548c
--- /dev/null
+++ b/src/templates/sre-agent/infra/main.bicep
@@ -0,0 +1,137 @@
+// Copied from microsoft/sre-agent labs/starter-lab/infra/main.bicep and
+// updated for the FinOps Toolkit SRE Agent template:
+// - no azd environment dependency
+// - no Grubify sample application
+// - resource-group scoped target access
+
+targetScope = 'subscription'
+
+@description('Resource group that holds the SRE Agent resources.')
+param resourceGroupName string
+
+@description('SRE Agent name.')
+param agentName string
+
+@description('Primary location for all resources.')
+@allowed(['australiaeast', 'canadacentral', 'eastus2', 'francecentral', 'koreacentral', 'swedencentral', 'uksouth'])
+param location string = 'eastus2'
+
+@description('Resource groups the agent can observe or act on.')
+param targetResourceGroups array = []
+
+@description('Agent access level.')
+@allowed(['Low', 'High'])
+param accessLevel string = 'Low'
+
+@description('Agent action mode.')
+@allowed(['review', 'autonomous', 'readOnly'])
+param actionMode string = 'autonomous'
+
+@description('Agent upgrade channel.')
+@allowed(['Stable', 'Preview'])
+param upgradeChannel string = 'Preview'
+
+@description('Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal.')
+@allowed(['MicrosoftFoundry', 'Anthropic'])
+param defaultModelProvider string = 'MicrosoftFoundry'
+
+@description('Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider.')
+param defaultModelName string = 'Automatic'
+
+@description('Monthly agent unit limit.')
+@minValue(1)
+param monthlyAgentUnitLimit int = 10000
+
+@description('Agent experimental settings.')
+param experimentalSettings object = {
+ EnableSandboxGroup: true
+ EnableWorkspaceTools: true
+}
+
+@description('Azure resource tags.')
+param tags object = {}
+
+@description('Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment.')
+param finopsHubKustoClusterResourceId string = ''
+
+@description('Assign Reader on the deployment subscription to the agent managed identity. Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.')
+param enableSubscriptionReaderRole bool = false
+
+@description('Principal type of the deployer.')
+@allowed(['User', 'ServicePrincipal'])
+param deployerPrincipalType string = 'User'
+
+var targetRgs = union([resourceGroupName], targetResourceGroups)
+var agentResourceGroupId = subscriptionResourceId('Microsoft.Resources/resourceGroups', resourceGroupName)
+var targetRgIds = [for rgName in targetRgs: subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)]
+var namingSeed = toLower('${subscription().subscriptionId}|${agentResourceGroupId}|${agentName}')
+var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
+
+resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
+ name: resourceGroupName
+ location: location
+ tags: tags
+}
+
+module resources 'resources.bicep' = {
+ name: 'resources-deployment'
+ scope: rg
+ params: {
+ agentName: agentName
+ location: location
+ namingSeed: namingSeed
+ targetResourceGroupIds: targetRgIds
+ accessLevel: accessLevel
+ actionMode: actionMode
+ upgradeChannel: upgradeChannel
+ defaultModelProvider: defaultModelProvider
+ defaultModelName: defaultModelName
+ monthlyAgentUnitLimit: monthlyAgentUnitLimit
+ experimentalSettings: experimentalSettings
+ deployerPrincipalType: deployerPrincipalType
+ tags: tags
+ }
+}
+
+module targetRbac 'modules/resource-group-rbac.bicep' = [for rgName in targetRgs: {
+ name: 'target-rbac-${uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)), namingSeed)}'
+ scope: resourceGroup(rgName)
+ params: {
+ principalId: resources.outputs.agentPrincipalId
+ accessLevel: accessLevel
+ }
+}]
+
+resource subscriptionReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableSubscriptionReaderRole) {
+ name: guid(subscription().id, namingSeed, readerRoleId)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', readerRoleId)
+ principalId: resources.outputs.agentPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+var hasKustoCluster = !empty(finopsHubKustoClusterResourceId)
+var kustoClusterSubscriptionId = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[2] : ''
+var kustoClusterResourceGroupName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[4] : ''
+var kustoClusterName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[8] : ''
+
+module finopsHubKustoAllDatabasesViewerRbac 'modules/kusto-all-databases-viewer-rbac.bicep' = if (hasKustoCluster) {
+ name: 'kusto-rbac-${uniqueString(finopsHubKustoClusterResourceId, namingSeed)}'
+ scope: resourceGroup(kustoClusterSubscriptionId, kustoClusterResourceGroupName)
+ params: {
+ clusterName: kustoClusterName
+ principalApplicationId: resources.outputs.agentPrincipalId
+ principalTenantId: tenant().tenantId
+ principalAssignmentName: 'sre-agent-${uniqueString(finopsHubKustoClusterResourceId, namingSeed, 'all-db-viewer')}'
+ }
+}
+
+output AZURE_RESOURCE_GROUP string = rg.name
+output AZURE_LOCATION string = location
+output SRE_AGENT_NAME string = resources.outputs.agentName
+output SRE_AGENT_ENDPOINT string = resources.outputs.agentEndpoint
+output AGENT_PORTAL_URL string = resources.outputs.agentPortalUrl
+output SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID string = resources.outputs.agentPrincipalId
+output SYSTEM_MANAGED_IDENTITY_TENANT_ID string = resources.outputs.agentTenantId
+output LOG_ANALYTICS_WORKSPACE_ID string = resources.outputs.logAnalyticsWorkspaceId
diff --git a/src/templates/sre-agent/infra/modules/apply-extras.bicep b/src/templates/sre-agent/infra/modules/apply-extras.bicep
new file mode 100644
index 000000000..4cc76f86f
--- /dev/null
+++ b/src/templates/sre-agent/infra/modules/apply-extras.bicep
@@ -0,0 +1,110 @@
+targetScope = 'resourceGroup'
+
+@description('Location for the deployment script resources.')
+param location string
+
+@description('SRE Agent name.')
+param agentName string
+
+@description('SRE Agent data-plane endpoint.')
+param agentEndpoint string
+
+@description('Subscription that contains the SRE Agent.')
+param subscriptionId string
+
+@description('Public URI for the generated SRE Agent recipe package.')
+param recipePackageUri string
+
+@description('SHA256 hash of the recipe package for integrity verification.')
+param recipePackageSha256 string
+
+@description('Optional database-qualified Kusto connector URI.')
+param kustoConnectorUri string = ''
+
+@description('Forces the deployment script to run when the template is redeployed.')
+param forceUpdateTag string
+
+@description('Azure resource tags.')
+param tags object = {}
+
+var identityName = 'id-sre-apply-${uniqueString(resourceGroup().id, agentName)}'
+var scriptName = 'apply-sre-${uniqueString(resourceGroup().id, agentName)}'
+var sreAgentAdminRoleId = 'e79298df-d852-4c6d-84f9-5d13249d1e55'
+
+#disable-next-line BCP081
+resource sreAgent 'Microsoft.App/agents@2026-01-01' existing = {
+ name: agentName
+}
+
+resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
+ name: identityName
+ location: location
+ tags: tags
+}
+
+resource sreAgentAdminRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(sreAgent.id, identity.id, sreAgentAdminRoleId)
+ scope: sreAgent
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', sreAgentAdminRoleId)
+ principalId: identity.properties.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource applyExtras 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
+ name: scriptName
+ location: location
+ tags: tags
+ kind: 'AzurePowerShell'
+ identity: {
+ type: 'UserAssigned'
+ userAssignedIdentities: {
+ '${identity.id}': {}
+ }
+ }
+ properties: {
+ azPowerShellVersion: '11.0'
+ retentionInterval: 'PT1H'
+ cleanupPreference: 'OnSuccess'
+ timeout: 'PT2H'
+ forceUpdateTag: forceUpdateTag
+ scriptContent: loadTextContent('../scripts/Apply-SreAgentExtras.ps1')
+ environmentVariables: [
+ {
+ name: 'subscriptionId'
+ value: subscriptionId
+ }
+ {
+ name: 'resourceGroupName'
+ value: resourceGroup().name
+ }
+ {
+ name: 'agentName'
+ value: agentName
+ }
+ {
+ name: 'agentEndpoint'
+ value: agentEndpoint
+ }
+ {
+ name: 'recipePackageUri'
+ value: recipePackageUri
+ }
+ {
+ name: 'recipePackageSha256'
+ value: recipePackageSha256
+ }
+ {
+ name: 'kustoConnectorUri'
+ value: kustoConnectorUri
+ }
+ ]
+ }
+ dependsOn: [
+ sreAgentAdminRoleAssignment
+ ]
+}
+
+output identityName string = identity.name
+output scriptName string = applyExtras.name
diff --git a/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep b/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep
new file mode 100644
index 000000000..61baa4c44
--- /dev/null
+++ b/src/templates/sre-agent/infra/modules/kusto-all-databases-viewer-rbac.bicep
@@ -0,0 +1,26 @@
+@description('Kusto cluster name.')
+param clusterName string
+
+@description('Microsoft Entra application/client ID to assign to the Kusto cluster.')
+param principalApplicationId string
+
+@description('Principal tenant ID.')
+param principalTenantId string
+
+@description('Stable principal assignment name.')
+param principalAssignmentName string
+
+resource cluster 'Microsoft.Kusto/clusters@2024-04-13' existing = {
+ name: clusterName
+}
+
+resource allDatabasesViewer 'Microsoft.Kusto/clusters/principalAssignments@2024-04-13' = {
+ parent: cluster
+ name: principalAssignmentName
+ properties: {
+ principalId: principalApplicationId
+ principalType: 'App'
+ role: 'AllDatabasesViewer'
+ tenantId: principalTenantId
+ }
+}
diff --git a/src/templates/sre-agent/infra/modules/monitoring.bicep b/src/templates/sre-agent/infra/modules/monitoring.bicep
new file mode 100644
index 000000000..6a0102df1
--- /dev/null
+++ b/src/templates/sre-agent/infra/modules/monitoring.bicep
@@ -0,0 +1,39 @@
+@description('Location for resources')
+param location string
+
+@description('Log Analytics Workspace name')
+param logAnalyticsName string
+
+@description('Application Insights name')
+param appInsightsName string
+
+// Log Analytics Workspace
+resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
+ name: logAnalyticsName
+ location: location
+ properties: {
+ sku: {
+ name: 'PerGB2018'
+ }
+ retentionInDays: 30
+ }
+}
+
+// Application Insights (linked to Log Analytics)
+resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
+ name: appInsightsName
+ location: location
+ kind: 'web'
+ properties: {
+ Application_Type: 'web'
+ Request_Source: 'IbizaAIExtension'
+ WorkspaceResourceId: logAnalyticsWorkspace.id
+ }
+}
+
+// Outputs
+output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.id
+output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name
+output appInsightsId string = applicationInsights.id
+output appInsightsAppId string = applicationInsights.properties.AppId
+output appInsightsConnectionString string = applicationInsights.properties.ConnectionString
diff --git a/src/templates/sre-agent/infra/modules/resource-group-rbac.bicep b/src/templates/sre-agent/infra/modules/resource-group-rbac.bicep
new file mode 100644
index 000000000..15ede6c34
--- /dev/null
+++ b/src/templates/sre-agent/infra/modules/resource-group-rbac.bicep
@@ -0,0 +1,33 @@
+@description('Principal ID of the managed identity to assign roles to.')
+param principalId string
+
+@description('Agent access level.')
+@allowed(['Low', 'High'])
+param accessLevel string
+
+var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
+var monitoringReaderRoleId = '43d0d8ad-25c7-4714-9337-8ba259a9fe05'
+var logAnalyticsReaderRoleId = '73c42c96-874c-492b-b04d-ab87d138a893'
+var contributorRoleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
+
+var roleIds = accessLevel == 'High'
+ ? [
+ readerRoleId
+ monitoringReaderRoleId
+ logAnalyticsReaderRoleId
+ contributorRoleId
+ ]
+ : [
+ readerRoleId
+ monitoringReaderRoleId
+ logAnalyticsReaderRoleId
+ ]
+
+resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for roleId in roleIds: {
+ name: guid(resourceGroup().id, principalId, roleId)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId)
+ principalId: principalId
+ principalType: 'ServicePrincipal'
+ }
+}]
diff --git a/src/templates/sre-agent/infra/modules/sre-agent.bicep b/src/templates/sre-agent/infra/modules/sre-agent.bicep
new file mode 100644
index 000000000..ded39f596
--- /dev/null
+++ b/src/templates/sre-agent/infra/modules/sre-agent.bicep
@@ -0,0 +1,107 @@
+// Copied from microsoft/sre-agent labs/starter-lab/infra/modules/sre-agent.bicep
+// and updated for the FinOps Toolkit SRE Agent template.
+
+@description('Location for resources.')
+param location string
+
+@description('SRE Agent name.')
+param agentName string
+
+@description('Application Insights App ID.')
+param appInsightsAppId string
+
+@description('Application Insights connection string.')
+@secure()
+param appInsightsConnectionString string
+
+@description('Application Insights resource ID.')
+param appInsightsId string
+
+@description('Resource group IDs to add as managed resources.')
+param managedResourceGroupIds array
+
+@description('Agent access level.')
+param accessLevel string
+
+@description('Agent action mode.')
+param actionMode string
+
+@description('Agent upgrade channel.')
+param upgradeChannel string
+
+@description('Default SRE Agent model provider.')
+param defaultModelProvider string
+
+@description('Default SRE Agent model name.')
+param defaultModelName string
+
+@description('Monthly agent unit limit.')
+param monthlyAgentUnitLimit int
+
+@description('Agent experimental settings.')
+param experimentalSettings object
+
+@description('Azure resource tags.')
+param tags object = {}
+
+@description('Principal type of the deployer.')
+@allowed(['User', 'ServicePrincipal'])
+param deployerPrincipalType string = 'User'
+
+var sreAgentAdminRoleId = 'e79298df-d852-4c6d-84f9-5d13249d1e55'
+
+#disable-next-line BCP081
+resource sreAgent 'Microsoft.App/agents@2026-01-01' = {
+ name: agentName
+ location: location
+ tags: union(tags, {
+ 'hidden-link: /app-insights-resource-id': appInsightsId
+ source: 'microsoft-sre-agent-starter-lab'
+ 'finops-toolkit': 'sre-agent'
+ })
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ knowledgeGraphConfiguration: {
+ managedResources: managedResourceGroupIds
+ identity: 'system'
+ }
+ actionConfiguration: {
+ mode: actionMode
+ identity: 'system'
+ accessLevel: accessLevel
+ }
+ upgradeChannel: upgradeChannel
+ defaultModel: {
+ provider: defaultModelProvider
+ name: defaultModelName
+ }
+ monthlyAgentUnitLimit: monthlyAgentUnitLimit
+ experimentalSettings: experimentalSettings
+ mcpServers: []
+ logConfiguration: {
+ applicationInsightsConfiguration: {
+ appId: appInsightsAppId
+ connectionString: appInsightsConnectionString
+ }
+ }
+ }
+}
+
+resource sreAgentAdminRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid(sreAgent.id, deployer().objectId, sreAgentAdminRoleId)
+ scope: sreAgent
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', sreAgentAdminRoleId)
+ principalId: deployer().objectId
+ principalType: deployerPrincipalType
+ }
+}
+
+output agentName string = sreAgent.name
+output agentId string = sreAgent.id
+output agentEndpoint string = sreAgent.properties.agentEndpoint
+output agentPortalUrl string = 'https://sre.azure.com/#/agent/${subscription().subscriptionId}/${resourceGroup().name}/${sreAgent.name}'
+output agentPrincipalId string = sreAgent.identity.principalId
+output agentTenantId string = sreAgent.identity.tenantId
diff --git a/src/templates/sre-agent/infra/resources.bicep b/src/templates/sre-agent/infra/resources.bicep
new file mode 100644
index 000000000..3107d9dee
--- /dev/null
+++ b/src/templates/sre-agent/infra/resources.bicep
@@ -0,0 +1,83 @@
+// Copied from microsoft/sre-agent labs/starter-lab/infra/resources.bicep and
+// updated for the FinOps Toolkit SRE Agent template.
+
+@description('SRE Agent name.')
+param agentName string
+
+@description('Location for all resources.')
+param location string
+
+@description('Deterministic naming seed built from subscription ID, agent resource group ID, and agent name.')
+param namingSeed string
+
+@description('Resource group IDs shown as managed resources in the SRE Agent.')
+param targetResourceGroupIds array
+
+@description('Agent access level.')
+param accessLevel string
+
+@description('Agent action mode.')
+param actionMode string
+
+@description('Agent upgrade channel.')
+param upgradeChannel string
+
+@description('Default SRE Agent model provider.')
+param defaultModelProvider string
+
+@description('Default SRE Agent model name.')
+param defaultModelName string
+
+@description('Monthly agent unit limit.')
+param monthlyAgentUnitLimit int
+
+@description('Agent experimental settings.')
+param experimentalSettings object
+
+@description('Principal type of the deployer.')
+@allowed(['User', 'ServicePrincipal'])
+param deployerPrincipalType string = 'User'
+
+@description('Azure resource tags.')
+param tags object = {}
+
+var uniqueSuffix = uniqueString(namingSeed)
+var logAnalyticsName = 'law-${uniqueSuffix}'
+var appInsightsName = 'appi-${uniqueSuffix}'
+
+module monitoring 'modules/monitoring.bicep' = {
+ name: 'monitoring'
+ params: {
+ location: location
+ logAnalyticsName: logAnalyticsName
+ appInsightsName: appInsightsName
+ }
+}
+
+module sreAgent 'modules/sre-agent.bicep' = {
+ name: 'sre-agent'
+ params: {
+ location: location
+ agentName: agentName
+ appInsightsAppId: monitoring.outputs.appInsightsAppId
+ appInsightsConnectionString: monitoring.outputs.appInsightsConnectionString
+ appInsightsId: monitoring.outputs.appInsightsId
+ managedResourceGroupIds: targetResourceGroupIds
+ accessLevel: accessLevel
+ actionMode: actionMode
+ upgradeChannel: upgradeChannel
+ defaultModelProvider: defaultModelProvider
+ defaultModelName: defaultModelName
+ monthlyAgentUnitLimit: monthlyAgentUnitLimit
+ experimentalSettings: experimentalSettings
+ deployerPrincipalType: deployerPrincipalType
+ tags: tags
+ }
+}
+
+output agentName string = sreAgent.outputs.agentName
+output agentEndpoint string = sreAgent.outputs.agentEndpoint
+output agentPortalUrl string = sreAgent.outputs.agentPortalUrl
+output agentPrincipalId string = sreAgent.outputs.agentPrincipalId
+output agentTenantId string = sreAgent.outputs.agentTenantId
+output logAnalyticsWorkspaceId string = monitoring.outputs.logAnalyticsWorkspaceId
diff --git a/src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1 b/src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1
new file mode 100644
index 000000000..85f6e9daf
--- /dev/null
+++ b/src/templates/sre-agent/infra/scripts/Apply-SreAgentExtras.ps1
@@ -0,0 +1,330 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+$ErrorActionPreference = 'Stop'
+
+function Get-RequiredEnv($Name) {
+ $value = [Environment]::GetEnvironmentVariable($Name)
+ if ([string]::IsNullOrWhiteSpace($value)) {
+ throw "Missing required environment variable: $Name"
+ }
+ return $value
+}
+
+function Get-OptionalEnv($Name) {
+ return [Environment]::GetEnvironmentVariable($Name)
+}
+
+function Get-TempRoot() {
+ $tempRoot = [System.IO.Path]::GetTempPath()
+ if ([string]::IsNullOrWhiteSpace($tempRoot)) {
+ $tempRoot = Get-OptionalEnv 'AZ_SCRIPTS_PATH_OUTPUT_DIRECTORY'
+ }
+ if ([string]::IsNullOrWhiteSpace($tempRoot)) {
+ $tempRoot = (Get-Location).Path
+ }
+ return $tempRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
+}
+
+function ConvertTo-BodyJson($Value) {
+ return ($Value | ConvertTo-Json -Depth 100 -Compress)
+}
+
+function Get-PropertyValue($Object, [string[]]$Names, $Default = '') {
+ foreach ($name in $Names) {
+ if ($null -ne $Object -and $Object.PSObject.Properties[$name] -and $null -ne $Object.$name) {
+ return $Object.$name
+ }
+ }
+ return $Default
+}
+
+function Get-HttpStatusCode($Exception) {
+ if ($Exception.Response -and $Exception.Response.StatusCode) {
+ return [int]$Exception.Response.StatusCode
+ }
+ return $null
+}
+
+function Invoke-WithRetry([scriptblock]$Action, [string]$Label, [int]$MaxAttempts = 5, [int]$DelaySeconds = 15) {
+ for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
+ try {
+ return & $Action
+ }
+ catch {
+ $statusCode = Get-HttpStatusCode $_.Exception
+ if ($statusCode -and $statusCode -notin @(401, 403, 408, 429, 500, 502, 503, 504)) {
+ throw
+ }
+ if ($attempt -eq $MaxAttempts) {
+ throw
+ }
+ Write-Output "$Label attempt $attempt/$MaxAttempts failed: $($_.Exception.Message)"
+ Start-Sleep -Seconds $DelaySeconds
+ }
+ }
+}
+
+function Invoke-JsonRest([string]$Method, [string]$Uri, [string]$Token, $Body = $null) {
+ $headers = @{
+ Authorization = "Bearer $Token"
+ 'Content-Type' = 'application/json'
+ }
+ $parameters = @{
+ Method = $Method
+ Uri = $Uri
+ Headers = $headers
+ }
+ if ($null -ne $Body) {
+ $parameters.Body = ConvertTo-BodyJson $Body
+ }
+ return Invoke-RestMethod @parameters
+}
+
+function Get-KnowledgeSourceName([string]$FileName) {
+ $name = [IO.Path]::GetFileName($FileName).ToLowerInvariant() -replace '[^a-z0-9-]+', '-'
+ $name = $name -replace '-+', '-'
+ return $name.Trim('-')
+}
+
+function Get-Collection($Value) {
+ if ($null -eq $Value) {
+ return @()
+ }
+ return @($Value)
+}
+
+function Convert-TokenToString($Token) {
+ if ($Token -is [Security.SecureString]) {
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token)
+ try {
+ return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
+ }
+ finally {
+ [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
+ }
+ }
+ return [string]$Token
+}
+
+function Get-ArmAccessTokenString() {
+ return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://management.azure.com/').Token
+}
+
+function Get-SreAccessTokenString() {
+ return Convert-TokenToString (Get-AzAccessToken -ResourceUrl 'https://azuresre.dev').Token
+}
+
+$subscriptionId = Get-RequiredEnv 'subscriptionId'
+$resourceGroupName = Get-RequiredEnv 'resourceGroupName'
+$agentName = Get-RequiredEnv 'agentName'
+$agentEndpoint = (Get-RequiredEnv 'agentEndpoint').TrimEnd('/')
+$recipePackageUri = Get-RequiredEnv 'recipePackageUri'
+$recipePackageSha256 = Get-RequiredEnv 'recipePackageSha256'
+$kustoConnectorUri = Get-OptionalEnv 'kustoConnectorUri'
+$armApiVersion = '2025-05-01-preview'
+$armBase = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.App/agents/$agentName"
+
+Write-Output 'Connecting with deployment script managed identity...'
+Connect-AzAccount -Identity | Out-Null
+Set-AzContext -Subscription $subscriptionId | Out-Null
+
+$tempRoot = Get-TempRoot
+$workRoot = Join-Path $tempRoot 'sre-agent-recipe'
+$zipPath = Join-Path $tempRoot 'sre-agent-recipe.zip'
+Remove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue
+New-Item -Path $workRoot -ItemType Directory -Force | Out-Null
+
+Write-Output "Validating SRE Agent recipe package URI: $recipePackageUri"
+[uri]$parsedUri = $recipePackageUri
+if ($parsedUri.Scheme -ne 'https') {
+ throw "Recipe package URI must use https scheme, got: $($parsedUri.Scheme)"
+}
+
+$allowedHosts = @(
+ 'raw.githubusercontent.com',
+ 'github.com',
+ 'aka.ms'
+)
+
+if ($allowedHosts -notcontains $parsedUri.Host -and $parsedUri.Host -notlike '*.blob.core.windows.net') {
+ throw "Recipe package host '$($parsedUri.Host)' is not in the allowlist. Allowed: raw.githubusercontent.com, github.com, aka.ms, *.blob.core.windows.net"
+}
+
+Write-Output "Downloading SRE Agent recipe package: $recipePackageUri"
+Invoke-WithRetry -Label 'download recipe package' -Action {
+ Invoke-WebRequest -Uri $recipePackageUri -OutFile $zipPath
+} | Out-Null
+
+Write-Output "Verifying recipe package integrity..."
+$actualHash = (Get-FileHash -Algorithm SHA256 -Path $zipPath).Hash
+if ($actualHash -ne $recipePackageSha256) {
+ throw "Recipe package hash mismatch: expected $recipePackageSha256 got $actualHash"
+}
+
+Expand-Archive -Path $zipPath -DestinationPath $workRoot -Force
+
+$extrasPath = Join-Path $workRoot 'extras.json'
+if (-not (Test-Path $extrasPath)) {
+ throw "Recipe package did not contain extras.json"
+}
+
+$extras = Get-Content $extrasPath -Raw | ConvertFrom-Json -Depth 100
+Write-Output "Loaded recipe package from $extrasPath"
+
+if ([string]::IsNullOrWhiteSpace($kustoConnectorUri)) {
+ $extras.connectors = @(
+ Get-Collection $extras.connectors | Where-Object {
+ $type = Get-PropertyValue $_.properties @('dataConnectorType', 'type')
+ $type -ine 'Kusto'
+ }
+ )
+}
+else {
+ foreach ($connector in Get-Collection $extras.connectors) {
+ $type = Get-PropertyValue $connector.properties @('dataConnectorType', 'type')
+ if ($type -ieq 'Kusto') {
+ $connector.properties.dataSource = $kustoConnectorUri
+ if (-not $connector.properties.PSObject.Properties['identity']) {
+ $connector.properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'
+ }
+ }
+ }
+}
+
+function Invoke-ArmPutConnector([string]$Name, $Body) {
+ $uri = "$armBase/connectors/$Name`?api-version=$armApiVersion"
+ Invoke-WithRetry -Label "ARM PUT connector $Name" -Action {
+ Invoke-JsonRest -Method PUT -Uri $uri -Token (Get-ArmAccessTokenString) -Body $Body
+ } | Out-Null
+ Write-Output " ARM PUT connectors/${Name}: ok"
+}
+
+function Invoke-SreApi([string]$Method, [string]$Path, $Body = $null) {
+ $uri = "$agentEndpoint$Path"
+ return Invoke-WithRetry -Label "$Method $Path" -MaxAttempts 21 -DelaySeconds 30 -Action {
+ Invoke-JsonRest -Method $Method -Uri $uri -Token (Get-SreAccessTokenString) -Body $Body
+ }
+}
+
+function Invoke-ExtendedPut([string]$Kind, [string]$Name, [string]$Type, $Properties) {
+ $encodedName = [Uri]::EscapeDataString($Name)
+ $body = @{
+ name = $Name
+ type = $Type
+ tags = @()
+ properties = $Properties
+ }
+ Invoke-SreApi -Method PUT -Path "/api/v2/extendedAgent/$Kind/$encodedName" -Body $body | Out-Null
+ Write-Output " PUT $Kind/${Name}: ok"
+}
+
+Write-Output 'Step 1/7: Applying connectors...'
+foreach ($connector in Get-Collection $extras.connectors) {
+ $name = [string]$connector.name
+ $properties = $connector.properties
+ if (-not $properties.PSObject.Properties['identity']) {
+ $properties | Add-Member -MemberType NoteProperty -Name identity -Value 'system'
+ }
+ Invoke-ArmPutConnector -Name $name -Body @{ properties = $properties }
+}
+
+Write-Output 'Step 2/7: Configuring built-in tools...'
+$overrides = Get-Collection $extras.builtInTools.overrides
+if ($overrides.Count -gt 0) {
+ $body = @{
+ overrides = @($overrides | ForEach-Object {
+ @{
+ name = $_.name
+ enabled = [bool]$_.enabled
+ }
+ })
+ }
+ Invoke-SreApi -Method POST -Path '/api/v2/agent/tools/configure' -Body $body | Out-Null
+ Write-Output " built-in tools configured: $($overrides.Count) overrides"
+}
+
+Write-Output 'Step 3/7: Uploading knowledge sources...'
+foreach ($item in Get-Collection $extras.knowledgeItems) {
+ $sourceName = Get-KnowledgeSourceName ([string]$item.name)
+ $bytes = [Text.Encoding]::UTF8.GetBytes([string]$item.content)
+ $body = @{
+ properties = @{
+ dataConnectorType = 'KnowledgeFile'
+ dataSource = $sourceName
+ extendedProperties = @{
+ displayName = [string]$item.name
+ fileName = [string]$item.name
+ fileContent = [Convert]::ToBase64String($bytes)
+ contentType = [string](Get-PropertyValue $item @('contentType') 'application/octet-stream')
+ }
+ }
+ }
+ Invoke-ArmPutConnector -Name $sourceName -Body $body
+ Start-Sleep -Seconds 5
+}
+
+Write-Output 'Step 4/7: Applying tools...'
+foreach ($tool in Get-Collection $extras.tools) {
+ $name = [string]$tool.metadata.name
+ $properties = $tool.spec
+ if ($properties.type -eq 'PythonTool') {
+ $properties.type = 'PythonFunctionTool'
+ }
+ Invoke-ExtendedPut -Kind 'tools' -Name $name -Type 'ExtendedAgentTool' -Properties $properties
+}
+
+Write-Output 'Step 5/7: Applying skills...'
+foreach ($skill in Get-Collection $extras.skills) {
+ $name = [string]$skill.metadata.name
+ $tools = @()
+ if ($skill.metadata.spec -and $skill.metadata.spec.tools) {
+ $tools = Get-Collection $skill.metadata.spec.tools
+ }
+ $properties = @{
+ name = $name
+ description = [string](Get-PropertyValue $skill.metadata @('description'))
+ tools = $tools
+ skillContent = [string](Get-PropertyValue $skill @('skillContent'))
+ additionalFiles = @()
+ }
+ Invoke-ExtendedPut -Kind 'skills' -Name $name -Type 'Skill' -Properties $properties
+}
+
+Write-Output 'Step 6/7: Applying subagents...'
+foreach ($subagent in Get-Collection $extras.subagents) {
+ $name = [string]$subagent.metadata.name
+ Invoke-ExtendedPut -Kind 'agents' -Name $name -Type 'ExtendedAgent' -Properties $subagent.spec
+}
+
+Write-Output 'Step 7/7: Applying scheduled tasks...'
+$scheduledTasks = Get-Collection $extras.scheduledTasks
+if ($scheduledTasks.Count -gt 0) {
+ $existing = Invoke-SreApi -Method GET -Path '/api/v1/scheduledtasks'
+ foreach ($task in $scheduledTasks) {
+ $name = [string]$task.metadata.name
+ foreach ($match in (Get-Collection $existing | Where-Object { $_.name -eq $name })) {
+ if ($match.id) {
+ Invoke-SreApi -Method DELETE -Path "/api/v1/scheduledtasks/$($match.id)" | Out-Null
+ Write-Output " deleted existing scheduledtasks/$name"
+ }
+ }
+ }
+
+ foreach ($task in $scheduledTasks) {
+ $name = [string]$task.metadata.name
+ $spec = $task.spec
+ $properties = @{
+ name = [string](Get-PropertyValue $spec @('name') $name)
+ description = [string](Get-PropertyValue $spec @('description'))
+ cronExpression = [string](Get-PropertyValue $spec @('schedule', 'cronExpression', 'cron_expression'))
+ agentPrompt = [string](Get-PropertyValue $spec @('prompt', 'agentPrompt', 'agent_prompt'))
+ agentMode = [string](Get-PropertyValue $spec @('mode', 'agentMode', 'agent_mode') 'Review')
+ isEnabled = [bool](Get-PropertyValue $spec @('enabled') $true)
+ agent = [string](Get-PropertyValue $spec @('agent'))
+ }
+ Invoke-ExtendedPut -Kind 'scheduledtasks' -Name $name -Type 'ScheduledTask' -Properties $properties
+ }
+}
+
+Write-Output 'SRE Agent extras complete.'
diff --git a/src/templates/sre-agent/main.bicep b/src/templates/sre-agent/main.bicep
new file mode 100644
index 000000000..373ccd0e3
--- /dev/null
+++ b/src/templates/sre-agent/main.bicep
@@ -0,0 +1,173 @@
+// One-click portal entry point for the FinOps Toolkit SRE Agent.
+// The CLI path keeps using infra/main.bicep directly.
+
+targetScope = 'subscription'
+
+@description('Resource group that holds the SRE Agent resources.')
+param resourceGroupName string
+
+@description('SRE Agent name.')
+param agentName string
+
+@description('Primary location for all resources.')
+@allowed(['australiaeast', 'canadacentral', 'eastus2', 'francecentral', 'koreacentral', 'swedencentral', 'uksouth'])
+param location string = 'eastus2'
+
+@description('Resource groups the agent can observe or act on. The agent resource group is always included.')
+param targetResourceGroups array = []
+
+@description('Comma-separated resource groups the agent can observe or act on. Used by the Azure portal form.')
+param targetResourceGroupNames string = ''
+
+@description('Optional database-qualified FinOps Hub Kusto connector URI. Example: https://..kusto.windows.net/Hub')
+param finopsHubKustoConnectorUri string = ''
+
+@description('Optional. FinOps Hub Azure Data Explorer cluster resource ID for Kusto viewer assignment.')
+param finopsHubKustoClusterResourceId string = ''
+
+@description('Agent access level. Low (read-only) is recommended for reporting and analysis without modification risk.')
+@allowed(['Low', 'High'])
+param accessLevel string = 'Low'
+
+@description('Agent action mode.')
+@allowed(['review', 'autonomous', 'readOnly'])
+param actionMode string = 'autonomous'
+
+@description('Agent upgrade channel.')
+@allowed(['Stable', 'Preview'])
+param upgradeChannel string = 'Preview'
+
+@description('Default SRE Agent model provider. MicrosoftFoundry maps to the Azure OpenAI provider in the SRE Agent portal.')
+@allowed(['MicrosoftFoundry', 'Anthropic'])
+param defaultModelProvider string = 'MicrosoftFoundry'
+
+@description('Default SRE Agent model name. Automatic lets SRE Agent route to the appropriate model within the selected provider.')
+param defaultModelName string = 'Automatic'
+
+@description('Monthly agent unit limit.')
+@minValue(1)
+param monthlyAgentUnitLimit int = 10000
+
+@description('Agent experimental settings.')
+param experimentalSettings object = {
+ EnableSandboxGroup: true
+ EnableWorkspaceTools: true
+}
+
+@description('Assign Reader on the deployment subscription to the agent managed identity. Optional — required only for subscription-wide ARM-backed reports; otherwise scope reads via target resource groups.')
+param enableSubscriptionReaderRole bool = false
+
+@description('Public URI for the generated SRE Agent recipe package. Deploy-to-Azure links derive this from the template URI.')
+param recipePackageUri string = uri(any(deployment()).properties.templateLink.uri, 'sre-agent-recipe.zip')
+
+@description('SHA256 hash of the recipe package for integrity verification.')
+param recipePackageSha256 string = 'PLACEHOLDER_RECIPE_PACKAGE_SHA256'
+
+@description('Forces the recipe deployment script to run when the template is redeployed.')
+param forceUpdateTag string = utcNow()
+
+@description('Azure resource tags.')
+param tags object = {
+ 'finops-toolkit': 'sre-agent'
+ source: 'microsoft-finops-toolkit'
+}
+
+@description('Principal type of the deployer.')
+@allowed(['User', 'ServicePrincipal'])
+param deployerPrincipalType string = 'User'
+
+var rawTargetResourceGroups = split(replace(targetResourceGroupNames, ' ', ''), ',')
+var parsedTargetResourceGroups = filter(rawTargetResourceGroups, rgName => !empty(rgName))
+var targetRgs = union([resourceGroupName], targetResourceGroups, parsedTargetResourceGroups)
+var agentResourceGroupId = subscriptionResourceId('Microsoft.Resources/resourceGroups', resourceGroupName)
+var targetRgIds = [for rgName in targetRgs: subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)]
+var namingSeed = toLower('${subscription().subscriptionId}|${agentResourceGroupId}|${agentName}')
+var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
+var hasKustoCluster = !empty(finopsHubKustoClusterResourceId)
+var kustoClusterSubscriptionId = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[2] : ''
+var kustoClusterResourceGroupName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[4] : ''
+var kustoClusterName = hasKustoCluster ? split(finopsHubKustoClusterResourceId, '/')[8] : ''
+
+resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
+ name: resourceGroupName
+ location: location
+ tags: tags
+}
+
+module resources 'infra/resources.bicep' = {
+ name: 'resources-deployment'
+ scope: rg
+ params: {
+ agentName: agentName
+ location: location
+ namingSeed: namingSeed
+ targetResourceGroupIds: targetRgIds
+ accessLevel: accessLevel
+ actionMode: actionMode
+ upgradeChannel: upgradeChannel
+ defaultModelProvider: defaultModelProvider
+ defaultModelName: defaultModelName
+ monthlyAgentUnitLimit: monthlyAgentUnitLimit
+ experimentalSettings: experimentalSettings
+ deployerPrincipalType: deployerPrincipalType
+ tags: tags
+ }
+}
+
+module targetRbac 'infra/modules/resource-group-rbac.bicep' = [for rgName in targetRgs: {
+ name: 'target-rbac-${uniqueString(toLower(subscriptionResourceId('Microsoft.Resources/resourceGroups', rgName)), namingSeed)}'
+ scope: resourceGroup(rgName)
+ params: {
+ principalId: resources.outputs.agentPrincipalId
+ accessLevel: accessLevel
+ }
+}]
+
+resource subscriptionReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (enableSubscriptionReaderRole) {
+ name: guid(subscription().id, namingSeed, readerRoleId)
+ properties: {
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', readerRoleId)
+ principalId: resources.outputs.agentPrincipalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+module finopsHubKustoAllDatabasesViewerRbac 'infra/modules/kusto-all-databases-viewer-rbac.bicep' = if (hasKustoCluster) {
+ name: 'kusto-rbac-${uniqueString(finopsHubKustoClusterResourceId, namingSeed)}'
+ scope: resourceGroup(kustoClusterSubscriptionId, kustoClusterResourceGroupName)
+ params: {
+ clusterName: kustoClusterName
+ principalApplicationId: resources.outputs.agentPrincipalId
+ principalTenantId: tenant().tenantId
+ principalAssignmentName: 'sre-agent-${uniqueString(finopsHubKustoClusterResourceId, namingSeed, 'all-db-viewer')}'
+ }
+}
+
+module applyExtras 'infra/modules/apply-extras.bicep' = {
+ name: 'apply-extras'
+ scope: rg
+ params: {
+ location: location
+ agentName: agentName
+ agentEndpoint: resources.outputs.agentEndpoint
+ subscriptionId: subscription().subscriptionId
+ recipePackageUri: recipePackageUri
+ recipePackageSha256: recipePackageSha256
+ kustoConnectorUri: finopsHubKustoConnectorUri
+ forceUpdateTag: forceUpdateTag
+ tags: tags
+ }
+ dependsOn: [
+ targetRbac
+ ]
+}
+
+output AZURE_RESOURCE_GROUP string = rg.name
+output AZURE_LOCATION string = location
+output SRE_AGENT_NAME string = resources.outputs.agentName
+output SRE_AGENT_ENDPOINT string = resources.outputs.agentEndpoint
+output AGENT_PORTAL_URL string = resources.outputs.agentPortalUrl
+output SYSTEM_MANAGED_IDENTITY_PRINCIPAL_ID string = resources.outputs.agentPrincipalId
+output SYSTEM_MANAGED_IDENTITY_TENANT_ID string = resources.outputs.agentTenantId
+output LOG_ANALYTICS_WORKSPACE_ID string = resources.outputs.logAnalyticsWorkspaceId
+output APPLY_EXTRAS_SCRIPT string = applyExtras.outputs.scriptName
diff --git a/src/templates/sre-agent/package-manifest.json b/src/templates/sre-agent/package-manifest.json
new file mode 100644
index 000000000..b3bde6c79
--- /dev/null
+++ b/src/templates/sre-agent/package-manifest.json
@@ -0,0 +1,21 @@
+{
+ "deployment": {
+ "Files": [
+ {
+ "sourceFolder": ".",
+ "source": "azuredeploy.json",
+ "destination": "azuredeploy.json"
+ },
+ {
+ "sourceFolder": ".",
+ "source": "createUiDefinition.json",
+ "destination": "createUiDefinition.json"
+ },
+ {
+ "sourceFolder": "assets",
+ "source": "sre-agent-recipe.zip",
+ "destination": "sre-agent-recipe.zip"
+ }
+ ]
+ }
+}
diff --git a/src/templates/sre-agent/recipes/finops-hub/agent.json b/src/templates/sre-agent/recipes/finops-hub/agent.json
index a2a90bbe8..b2b21d260 100644
--- a/src/templates/sre-agent/recipes/finops-hub/agent.json
+++ b/src/templates/sre-agent/recipes/finops-hub/agent.json
@@ -1,7 +1,7 @@
{
"access": {
- "accessLevel": "High",
- "actionMode": "Autonomous"
+ "accessLevel": "Low",
+ "actionMode": "autonomous"
},
"upgradeChannel": "Preview",
"defaultModelProvider": "MicrosoftFoundry",
diff --git a/src/templates/sre-agent/recipes/finops-hub/expected-config.json b/src/templates/sre-agent/recipes/finops-hub/expected-config.json
index 9e6e49a04..d4e041716 100644
--- a/src/templates/sre-agent/recipes/finops-hub/expected-config.json
+++ b/src/templates/sre-agent/recipes/finops-hub/expected-config.json
@@ -1,6 +1,6 @@
{
"agent": {
- "accessLevel": "High",
+ "accessLevel": "Low",
"actionMode": "autonomous",
"upgradeChannel": "Preview",
"defaultModelProvider": "MicrosoftFoundry",
@@ -9,9 +9,7 @@
"EnableWorkspaceTools"
]
},
- "subscriptionRoleAssignments": [
- "Reader"
- ],
+ "subscriptionRoleAssignments": [],
"builtInTools": {
"enabled": [
"PlotAreaChartWithCorrelation",