From 5e9cd4d55f09678a1227c5b77f4fafa6e78b5539 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:27:01 -0400 Subject: [PATCH 01/33] Ignore local worktree directory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1f6136e1..e36e7992 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ +.worktrees/ # ASP.NET Scaffolding ScaffoldingReadMe.txt @@ -397,4 +398,3 @@ static/ devTools/ node_modules/ *.tsbuildinfo - From ca010610af77abcbe5ed0faa6b85e355dfb5ceaf Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:04:43 -0400 Subject: [PATCH 02/33] Document sample maturity taxonomy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- samples/README.md | 63 ++++++++++++++++++++++++--- samples/SAMPLE_README_TEMPLATE.md | 72 +++++++++++++++++++++++++++++++ samples/dotnet/README.md | 34 ++++++++------- samples/nodejs/README.md | 32 +++++++------- samples/python/README.md | 29 +++++++------ 6 files changed, 180 insertions(+), 52 deletions(-) create mode 100644 samples/SAMPLE_README_TEMPLATE.md diff --git a/README.md b/README.md index 72fbb8e4..5d733e6a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Use the Agents SDK to: ## Get Started -You can get started right here by scolling up and clicking the 'samples' folder in this repo, selecting your language and reviewing the samples list before picking which one you want to start with (QuickStart is recommended for fundamentals). +You can get started right here by opening the [samples catalog](samples/README.md), selecting your language, and reviewing each sample's maturity tier before picking where to start. The QuickStart samples are recommended for fundamentals. Microsoft 365 Copilot is one of many channels supported by this SDK, and a *Microsoft 365 Copilot subscription is *not* required* to get started with or use this SDK unless your working specifically with that channel (or others, that require a subscription/service to use their client). diff --git a/samples/README.md b/samples/README.md index c223fb90..21ffaf3b 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,9 +1,58 @@ -# Microsoft 365 Agents SDK Samples list +# Microsoft 365 Agents SDK Samples -This table contains links to the samples available for each language. +This catalog helps you choose a sample by language, scenario, and expected maturity. Most samples in this repository are starter samples: they are intentionally small, focused examples for learning one SDK concept or integration path. They are not production reference implementations unless explicitly labeled that way. -|Language | README | -|---------|-------------| -|.Net|[dotnet](dotnet/README.md)| -|JavaScript|[nodejs](nodejs/README.md)| -|Python|[python](python/README.md)| \ No newline at end of file +## Sample maturity tiers + +| Tier | Use when you want to | What to expect | +|------|----------------------|----------------| +| **Tier 1: QuickStart** | Learn the SDK messaging loop and get a minimal agent running quickly. | Minimal code, local setup instructions, and explicit development-time shortcuts. | +| **Tier 2: Scenario starter** | Start from a realistic feature or integration scenario. | A focused sample for auth, telemetry, cards, streaming, Copilot Studio, tool orchestration, or another SDK capability. | +| **Tier 3: Production reference** | Understand a deployable architecture pattern. | Durable state, managed identity or secret management, health checks, observability, tests, infrastructure as code, deployment guidance, and a runbook. | + +## Choose a sample + +| Language | Sample list | Best first sample | +|----------|-------------|-------------------| +| .NET | [dotnet](dotnet/README.md) | [QuickStart](dotnet/quickstart/README.md) | +| JavaScript | [nodejs](nodejs/README.md) | [QuickStart](nodejs/quickstart/README.md) | +| Python | [python](python/README.md) | [Quickstart](python/quickstart/README.md) | + +## Starter sample contract + +Every starter sample should make its scope clear: + +- State the maturity tier, language, scenario, supported channels, and approximate setup time. +- Name intentional shortcuts, such as in-memory state, local-only secrets, disabled token validation, or omitted retry policies. +- Explain what the sample does and does not demonstrate. +- Link to the next sample or documentation a developer should use when moving toward production. +- Include a lightweight smoke test or manual verification step that confirms the agent starts and responds. + +Use [SAMPLE_README_TEMPLATE.md](SAMPLE_README_TEMPLATE.md) when adding or refreshing sample documentation. + +## Current production reference coverage + +The repository currently emphasizes Tier 1 and Tier 2 samples. Before labeling a sample as Tier 3, it should include or link to: + +| Capability | Expected production-reference coverage | +|------------|----------------------------------------| +| Identity and secrets | Microsoft Entra configuration, managed identity where possible, Key Vault or equivalent secret storage, and no checked-in secrets. | +| State and storage | Durable conversation or user state with local development and cloud deployment configuration. | +| Observability | Structured logs, traces, metrics, correlation IDs, and a documented telemetry backend. | +| Reliability | Error handling, retry/throttling guidance, and health/readiness endpoints. | +| Deployment | Infrastructure as code, CI/CD guidance, rollback notes, and environment-specific configuration. | +| Testing | Unit tests, integration or smoke tests, and clear local validation commands. | +| Operations | Troubleshooting notes, a short runbook, and known limitations. | + +## Starter gaps to prioritize + +The current catalog has strong coverage for quickstarts, authentication, OpenTelemetry, streaming, Copilot Studio integration, and orchestration scenarios. The next starter samples that would most improve the path from "hello world" to production are: + +| Gap | Why it matters | +|-----|----------------| +| Persistent state starter | Shows how to replace in-memory or process-local state with durable storage. | +| Configuration and secret management starter | Shows local development settings separately from cloud-hosted secret storage. | +| Error handling, retry, and throttling starter | Shows realistic handling for downstream API failures and rate limits. | +| Health and readiness starter | Shows the endpoints and checks needed by hosting platforms and deployment pipelines. | +| Evaluation and regression testing starter | Shows how to validate prompts, tools, and agent behavior before deployment. | +| Cross-language parity set | Keeps the core QuickStart, auth, telemetry, and storage paths consistent across .NET, JavaScript, and Python. | diff --git a/samples/SAMPLE_README_TEMPLATE.md b/samples/SAMPLE_README_TEMPLATE.md new file mode 100644 index 00000000..a3fcd65a --- /dev/null +++ b/samples/SAMPLE_README_TEMPLATE.md @@ -0,0 +1,72 @@ +# Sample README Template + +Use this template when adding a new Microsoft 365 Agents SDK sample or refreshing an existing sample README. Keep starter samples focused and explicit about what is intentionally omitted. + +````markdown +# + +> **Sample tier:** Tier 1: QuickStart | Tier 2: Scenario starter | Tier 3: Production reference +> **Language:** .NET | JavaScript | Python +> **Scenario:** +> **Estimated setup time:** +> **Supported channels:** + +## What this sample demonstrates + +- +- + +## What this sample does not demonstrate + +- +- + +## Prerequisites + +- +- +- + +## Configure the sample + +1. +1. + +## Run locally + +```bash + +``` + +## Verify the sample + +1. +1. +1. Confirm: `` + +## Intentional shortcuts + +| Shortcut | Why it is acceptable in this sample | Production alternative | +|----------|-------------------------------------|------------------------| +| | | | + +## Next steps + +- +- +- +```` + +## Tier 3 production-reference additions + +If the sample is labeled Tier 3, also include: + +- Architecture diagram or deployment topology. +- Durable state and storage configuration. +- Managed identity or equivalent non-local secret flow. +- Health/readiness endpoints. +- OpenTelemetry or equivalent structured observability. +- Unit, integration, and smoke test commands. +- Infrastructure-as-code deployment instructions. +- Rollback and troubleshooting notes. +- Known limits and support boundaries. diff --git a/samples/dotnet/README.md b/samples/dotnet/README.md index 3518c3b8..98b5b734 100644 --- a/samples/dotnet/README.md +++ b/samples/dotnet/README.md @@ -1,18 +1,20 @@ # Microsoft 365 .NET Agents SDK Samples list -|Name|Description|README| -|----|----|----| -|QuickStart/Empty Agent|Simplest agent|[quickstart](quickstart/README.md)| -|AutoSignIn|Simple OAuth agent using Graph|[auto-signin](auto-signin/README.md)| -|OBOAuthorization|OBO to Copilot Studio Agent |[obo-authorization](obo-authorization/README.md)| -|Semantic Kernel | WeatherAgent which demonstrates a multi-turn AI interaction |[semantic-kernel-multiturn](semantic-kernel-multiturn/README.md)| -|Streaming Agent |Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)| -|Copilot Studio Client|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| -|Copilot Studio Skill |Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| -|RetrievalBot Sample with Semantic Kernel|A simple Retrieval Agent that is hosted on an Asp.net core web service. |[RetrievalBot](RetrievalBot/README.md)| -|MultiAgent|Demonstrates multiple AgentApplication in the same host|[MultiAgent](multiagent/README.md)| -|GenesysHandoff|Demonstrates how a Microsoft Copilot Studio Agent (bot) can seamlessly **hand off a conversation to a live agent** in **Genesys Cloud**.|[GenesysHandoff](genesys-handoff/README.md)| -|Proactive|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)| -|OpenTelemetry Agent|Configures OTel tracing, metrics, and logging with OTLP export|[otel](otel/README.md)| -|Agent Framework|Weather agent built with Microsoft Agent Framework SDK|[Agent Framework](Agent%20Framework/README.md)| -|Copilot SDK|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| \ No newline at end of file +These samples are Tier 1 or Tier 2 starter samples unless a README explicitly labels the sample as a production reference. See the [sample maturity tiers](../README.md#sample-maturity-tiers) before copying a sample into an application. + +|Name|Tier|Description|README| +|----|----|----|----| +|QuickStart/Empty Agent|Tier 1: QuickStart|Simplest agent|[quickstart](quickstart/README.md)| +|AutoSignIn|Tier 2: Scenario starter|Simple OAuth agent using Graph|[auto-signin](auto-signin/README.md)| +|OBOAuthorization|Tier 2: Scenario starter|OBO to Copilot Studio Agent |[obo-authorization](obo-authorization/README.md)| +|Semantic Kernel | Tier 2: Scenario starter | WeatherAgent which demonstrates a multi-turn AI interaction |[semantic-kernel-multiturn](semantic-kernel-multiturn/README.md)| +|Streaming Agent |Tier 2: Scenario starter|Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)| +|Copilot Studio Client|Tier 2: Scenario starter|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| +|Copilot Studio Skill |Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| +|RetrievalBot Sample with Semantic Kernel|Tier 2: Scenario starter|A simple Retrieval Agent that is hosted on an Asp.net core web service. |[RetrievalBot](RetrievalBot/README.md)| +|MultiAgent|Tier 2: Scenario starter|Demonstrates multiple AgentApplication in the same host|[MultiAgent](multiagent/README.md)| +|GenesysHandoff|Tier 2: Scenario starter|Demonstrates how a Microsoft Copilot Studio Agent (bot) can seamlessly **hand off a conversation to a live agent** in **Genesys Cloud**.|[GenesysHandoff](genesys-handoff/README.md)| +|Proactive|Tier 2: Scenario starter|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)| +|OpenTelemetry Agent|Tier 2: Scenario starter|Configures OTel tracing, metrics, and logging with OTLP export|[otel](otel/README.md)| +|Agent Framework|Tier 2: Scenario starter|Weather agent built with Microsoft Agent Framework SDK|[Agent Framework](Agent%20Framework/README.md)| +|Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| diff --git a/samples/nodejs/README.md b/samples/nodejs/README.md index 71bafb2f..e574c09b 100644 --- a/samples/nodejs/README.md +++ b/samples/nodejs/README.md @@ -1,17 +1,19 @@ # Microsoft 365 Agents JavaScript SDK Samples list -|Name|Description|README| -|----|----|----| -|QuickStart/Empty Agent|Simplest agent|N/A| -|AutoSignIn|Simple OAuth agent using Graph|[auto-signin](auto-signin/README.md)| -|OBOAuthorization|OBO to Copilot Studio Agent|[obo-authorization](obo-authorization/README.md)| -|Integrate LangChain|WeatherAgent with LangChain|[langchain-multiturn](langchain-multiturn/README.md)| -|Streaming Agent|Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)| -|Copilot Studio Client|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| -|Copilot Studio Client - React Webchat|App to consume a Copilot Studio Agent integrated with a Webchat Client|[copilotstudio-webchat-react](copilotstudio-webchat-react/README.md)| -|Copilot Studio Webclient|Webclient for Copilot Studio integration|[copilotstudio-webclient](copilotstudio-webclient/README.md)| -|Cards Agent|Agent that uses rich cards to enhance conversation design |[cards](cards/README.md)| -|Multi-turn Prompt |Multi-turn dialog flow using prompts and state management |[multi-turn-prompt](multi-turn-prompt/README.md)| -|Copilot Studio Skill|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| -|Copilot SDK|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| -|OTel Agent|Agent with OTel instrumentation |[otel](otel/README.md)| +These samples are Tier 1 or Tier 2 starter samples unless a README explicitly labels the sample as a production reference. See the [sample maturity tiers](../README.md#sample-maturity-tiers) before copying a sample into an application. + +|Name|Tier|Description|README| +|----|----|----|----| +|QuickStart/Empty Agent|Tier 1: QuickStart|Simplest agent|[quickstart](quickstart/README.md)| +|AutoSignIn|Tier 2: Scenario starter|Simple OAuth agent using Graph|[auto-signin](auto-signin/README.md)| +|OBOAuthorization|Tier 2: Scenario starter|OBO to Copilot Studio Agent|[obo-authorization](obo-authorization/README.md)| +|Integrate LangChain|Tier 2: Scenario starter|WeatherAgent with LangChain|[langchain-multiturn](langchain-multiturn/README.md)| +|Streaming Agent|Tier 2: Scenario starter|Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)| +|Copilot Studio Client|Tier 2: Scenario starter|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| +|Copilot Studio Client - React Webchat|Tier 2: Scenario starter|App to consume a Copilot Studio Agent integrated with a Webchat Client|[copilotstudio-webchat-react](copilotstudio-webchat-react/README.md)| +|Copilot Studio Webclient|Tier 2: Scenario starter|Webclient for Copilot Studio integration|[copilotstudio-webclient](copilotstudio-webclient/README.md)| +|Cards Agent|Tier 2: Scenario starter|Agent that uses rich cards to enhance conversation design |[cards](cards/README.md)| +|Multi-turn Prompt |Tier 2: Scenario starter|Multi-turn dialog flow using prompts and state management |[multi-turn-prompt](multi-turn-prompt/README.md)| +|Copilot Studio Skill|Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| +|Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| +|OTel Agent|Tier 2: Scenario starter|Agent with OTel instrumentation |[otel](otel/README.md)| diff --git a/samples/python/README.md b/samples/python/README.md index 7819b4a8..ab0e503c 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -1,18 +1,20 @@ # Microsoft 365 Agents Python SDK Samples list -|Name|Description|README| -|----|----|----| -|Quickstart|Simplest agent|N/A| -|Auto Sign In|Simple OAuth agent using Graph and GitHub|[auto-signin](auto-signin/README.md)| -|OBO Authorization|OBO flow to access a Copilot Studio Agent|[obo-authorization](obo-authorization/README.md)| -|Semantic Kernel Integration|A weather agent built with Semantic Kernel|[semantic-kernel-multiturn](semantic-kernel-multiturn/README.md)| -|Streaming Agent|Streams OpenAI responses|[azureai-streaming](azureai-streaming/README.md)| -|Copilot Studio Client|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| -|Cards Agent|Agent that uses rich cards to enhance conversation design |[cards](cards/README.md)| -|Copilot Studio Skill|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| -|OpenTelemetry|Instrument an agent and consume telemetry via the Aspire dashboard|[opentelemetry](otel/README.md)| -|Agent Framework|Weather agent built with Microsoft Agent Framework SDK|[agent-framework](agent-framework/README.md)| -|Copilot SDK|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| +These samples are Tier 1 or Tier 2 starter samples unless a README explicitly labels the sample as a production reference. See the [sample maturity tiers](../README.md#sample-maturity-tiers) before copying a sample into an application. + +|Name|Tier|Description|README| +|----|----|----|----| +|Quickstart|Tier 1: QuickStart|Simplest agent|[quickstart](quickstart/README.md)| +|Auto Sign In|Tier 2: Scenario starter|Simple OAuth agent using Graph and GitHub|[auto-signin](auto-signin/README.md)| +|OBO Authorization|Tier 2: Scenario starter|OBO flow to access a Copilot Studio Agent|[obo-authorization](obo-authorization/README.md)| +|Semantic Kernel Integration|Tier 2: Scenario starter|A weather agent built with Semantic Kernel|[semantic-kernel-multiturn](semantic-kernel-multiturn/README.md)| +|Streaming Agent|Tier 2: Scenario starter|Streams OpenAI responses|[azureai-streaming](azureai-streaming/README.md)| +|Copilot Studio Client|Tier 2: Scenario starter|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| +|Cards Agent|Tier 2: Scenario starter|Agent that uses rich cards to enhance conversation design |[cards](cards/README.md)| +|Copilot Studio Skill|Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| +|OpenTelemetry|Tier 2: Scenario starter|Instrument an agent and consume telemetry via the Aspire dashboard|[opentelemetry](otel/README.md)| +|Agent Framework|Tier 2: Scenario starter|Weather agent built with Microsoft Agent Framework SDK|[agent-framework](agent-framework/README.md)| +|Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| ## Important Notice - Import Changes @@ -44,3 +46,4 @@ from microsoft_agents.authentication.msal import MsalAuth # Copilot Studio client from microsoft_agents.copilotstudio.client import CopilotClient +``` From 7dec38089175334d30c1e77ad99b83eb69c73b56 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:10:30 -0400 Subject: [PATCH 03/33] Add .NET persistent state starter - New Tier 2 sample: samples/dotnet/persistent-state - PersistentState.csproj references Microsoft.Agents.Storage.Blobs 1.5.* - Program.cs reads AZURE_BLOB_STORAGE_CONNECTION_STRING (fail-fast) and AZURE_BLOB_STORAGE_CONTAINER_NAME (default: agents-persistent-state), registers BlobsStorage as IStorage singleton - MyAgent.cs uses turnState.Conversation.GetValue/SetValue to maintain a per-conversation message counter across restarts - AspNetExtensions.cs, appsettings.json, launchSettings.json copied from QuickStart to preserve local auth and launch behavior - README follows SAMPLE_README_TEMPLATE.md; covers local connection string setup, Azurite emulation, container naming rules, verify steps, and managed identity / workload identity as the cloud deployment path - Project added to samples/dotnet/Samples.sln Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 144 ++++++++ .../persistent-state/AspNetExtensions.cs | 342 ++++++++++++++++++ samples/dotnet/persistent-state/MyAgent.cs | 43 +++ .../persistent-state/PersistentState.csproj | 16 + samples/dotnet/persistent-state/Program.cs | 53 +++ .../Properties/launchSettings.json | 12 + samples/dotnet/persistent-state/README.md | 99 +++++ .../dotnet/persistent-state/appsettings.json | 39 ++ 8 files changed, 748 insertions(+) create mode 100644 samples/dotnet/persistent-state/AspNetExtensions.cs create mode 100644 samples/dotnet/persistent-state/MyAgent.cs create mode 100644 samples/dotnet/persistent-state/PersistentState.csproj create mode 100644 samples/dotnet/persistent-state/Program.cs create mode 100644 samples/dotnet/persistent-state/Properties/launchSettings.json create mode 100644 samples/dotnet/persistent-state/README.md create mode 100644 samples/dotnet/persistent-state/appsettings.json diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index d336badd..377ecd93 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.4.11702.344 @@ -37,76 +38,219 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopilotSdk", "copilot-sdk\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlackAgent", "slackagent\SlackAgent.csproj", "{74277777-C4F9-2F3E-162A-02D8A8E4B702}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "persistent-state", "persistent-state", "{1A7EC4A9-7FBF-852D-259A-AE3D2D6005ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentState", "persistent-state\PersistentState.csproj", "{C40EE44E-A349-4B48-9556-AEA743E23092}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x64.ActiveCfg = Debug|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x64.Build.0 = Debug|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x86.ActiveCfg = Debug|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x86.Build.0 = Debug|Any CPU {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|Any CPU.ActiveCfg = Release|Any CPU {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|Any CPU.Build.0 = Release|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x64.ActiveCfg = Release|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x64.Build.0 = Release|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x86.ActiveCfg = Release|Any CPU + {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x86.Build.0 = Release|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x64.Build.0 = Debug|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x86.Build.0 = Debug|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|Any CPU.Build.0 = Release|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x64.ActiveCfg = Release|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x64.Build.0 = Release|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x86.ActiveCfg = Release|Any CPU + {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x86.Build.0 = Release|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|Any CPU.Build.0 = Debug|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x64.ActiveCfg = Debug|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x64.Build.0 = Debug|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x86.ActiveCfg = Debug|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x86.Build.0 = Debug|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|Any CPU.ActiveCfg = Release|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|Any CPU.Build.0 = Release|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x64.ActiveCfg = Release|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x64.Build.0 = Release|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x86.ActiveCfg = Release|Any CPU + {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x86.Build.0 = Release|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x64.Build.0 = Debug|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x86.Build.0 = Debug|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|Any CPU.Build.0 = Release|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x64.ActiveCfg = Release|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x64.Build.0 = Release|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x86.ActiveCfg = Release|Any CPU + {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x86.Build.0 = Release|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x64.Build.0 = Debug|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x86.Build.0 = Debug|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|Any CPU.Build.0 = Release|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x64.ActiveCfg = Release|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x64.Build.0 = Release|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x86.ActiveCfg = Release|Any CPU + {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x86.Build.0 = Release|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x64.Build.0 = Debug|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x86.ActiveCfg = Debug|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x86.Build.0 = Debug|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|Any CPU.Build.0 = Release|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x64.ActiveCfg = Release|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x64.Build.0 = Release|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x86.ActiveCfg = Release|Any CPU + {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x86.Build.0 = Release|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x64.Build.0 = Debug|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x86.Build.0 = Debug|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|Any CPU.Build.0 = Release|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x64.ActiveCfg = Release|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x64.Build.0 = Release|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x86.ActiveCfg = Release|Any CPU + {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x86.Build.0 = Release|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x64.Build.0 = Debug|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x86.Build.0 = Debug|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|Any CPU.Build.0 = Release|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x64.ActiveCfg = Release|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x64.Build.0 = Release|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x86.ActiveCfg = Release|Any CPU + {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x86.Build.0 = Release|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x64.ActiveCfg = Debug|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x64.Build.0 = Debug|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x86.ActiveCfg = Debug|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x86.Build.0 = Debug|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|Any CPU.ActiveCfg = Release|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|Any CPU.Build.0 = Release|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x64.ActiveCfg = Release|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x64.Build.0 = Release|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x86.ActiveCfg = Release|Any CPU + {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x86.Build.0 = Release|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x64.Build.0 = Debug|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x86.ActiveCfg = Debug|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x86.Build.0 = Debug|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|Any CPU.Build.0 = Release|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x64.ActiveCfg = Release|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x64.Build.0 = Release|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x86.ActiveCfg = Release|Any CPU + {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x86.Build.0 = Release|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x64.ActiveCfg = Debug|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x64.Build.0 = Debug|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x86.ActiveCfg = Debug|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x86.Build.0 = Debug|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|Any CPU.ActiveCfg = Release|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|Any CPU.Build.0 = Release|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x64.ActiveCfg = Release|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x64.Build.0 = Release|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x86.ActiveCfg = Release|Any CPU + {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x86.Build.0 = Release|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x64.ActiveCfg = Debug|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x64.Build.0 = Debug|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x86.Build.0 = Debug|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|Any CPU.Build.0 = Release|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x64.ActiveCfg = Release|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x64.Build.0 = Release|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x86.ActiveCfg = Release|Any CPU + {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x86.Build.0 = Release|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x64.ActiveCfg = Debug|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x64.Build.0 = Debug|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x86.ActiveCfg = Debug|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x86.Build.0 = Debug|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|Any CPU.Build.0 = Release|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x64.ActiveCfg = Release|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x64.Build.0 = Release|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x86.ActiveCfg = Release|Any CPU + {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x86.Build.0 = Release|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x64.ActiveCfg = Debug|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x64.Build.0 = Debug|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x86.ActiveCfg = Debug|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x86.Build.0 = Debug|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|Any CPU.ActiveCfg = Release|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|Any CPU.Build.0 = Release|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x64.ActiveCfg = Release|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x64.Build.0 = Release|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x86.ActiveCfg = Release|Any CPU + {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x86.Build.0 = Release|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x64.ActiveCfg = Debug|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x64.Build.0 = Debug|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x86.ActiveCfg = Debug|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x86.Build.0 = Debug|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|Any CPU.ActiveCfg = Release|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|Any CPU.Build.0 = Release|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x64.ActiveCfg = Release|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x64.Build.0 = Release|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x86.ActiveCfg = Release|Any CPU + {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x86.Build.0 = Release|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x64.ActiveCfg = Debug|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x64.Build.0 = Debug|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x86.ActiveCfg = Debug|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x86.Build.0 = Debug|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|Any CPU.Build.0 = Release|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x64.ActiveCfg = Release|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x64.Build.0 = Release|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x86.ActiveCfg = Release|Any CPU + {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C40EE44E-A349-4B48-9556-AEA743E23092} = {1A7EC4A9-7FBF-852D-259A-AE3D2D6005ED} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {12345678-1234-5678-9012-123456789012} EndGlobalSection diff --git a/samples/dotnet/persistent-state/AspNetExtensions.cs b/samples/dotnet/persistent-state/AspNetExtensions.cs new file mode 100644 index 00000000..e9c0b413 --- /dev/null +++ b/samples/dotnet/persistent-state/AspNetExtensions.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Validators; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens; + +public static class AspNetExtensions +{ + private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. + /// + /// The to add authentication services to. + /// The application configuration containing a section. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + /// + /// + /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and + /// all requests will be treated as unauthenticated. This is useful for local development only. + /// + /// + /// Minimum configuration for Azure Public cloud: + /// + /// "TokenValidation": { + /// "Audiences": [ "{{ClientId}}" ], + /// "TenantId": "{{TenantId}}" + /// } + /// + /// + /// + /// Minimum configuration for Azure Government cloud — add "IsGov": true: + /// + /// "TokenValidation": { + /// "Audiences": [ "{{ClientId}}" ], + /// "TenantId": "{{TenantId}}", + /// "IsGov": true + /// } + /// + /// Setting IsGov automatically selects the correct government-cloud issuer URLs and OpenID metadata + /// endpoints. See for the full list of defaults that are applied. + /// + /// + /// For China or other sovereign clouds, omit IsGov and set + /// , + /// , and + /// explicitly. + /// See for the full set of available settings. + /// + /// + public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation") + { + IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); + + if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + { + // Noop if TokenValidation section missing or disabled. + System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); + services.AddControllers(); + return; + } + + services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests using the supplied options. + /// + /// The to add authentication services to. + /// The fully populated to use. + public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) + { + AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); + services.AddControllers(); + + // Must have at least one Audience. + if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId"); + } + + // Audience values must be GUID's + foreach (var audience in validationOptions.Audiences) + { + if (!Guid.TryParse(audience, out _)) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID"); + } + } + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) + { + if (validationOptions.IsGov) + { + validationOptions.ValidIssuers = + [ + AuthenticationConstants.GovBotFrameworkTokenIssuer, + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0" + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + else + { + validationOptions.ValidIssuers = + [ + AuthenticationConstants.BotFrameworkTokenIssuer, + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + } + + // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. + if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl)) + { + validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; + } + + // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. + if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl)) + { + validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; + } + + var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval; + + _ = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidIssuers = validationOptions.ValidIssuers, + ValidAudiences = validationOptions.Audiences, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + }; + + // Using Microsoft.IdentityModel.Validators + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + options.Events = new JwtBearerEvents + { + // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. + OnMessageReceived = context => + { + string authorizationHeader = context.Request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authorizationHeader)) + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + return Task.CompletedTask; + } + + string[] parts = authorizationHeader?.Split(' ')!; + if (parts.Length != 2 || parts[0] != "Bearer") + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + return Task.CompletedTask; + } + + // Use JsonWebToken for lightweight issuer extraction without full token parsing + JsonWebToken token = new(parts[1]); + string issuer = token.Issuer; + + if (validationOptions.AzureBotServiceTokenHandling + && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + { + // Use the Azure Bot authority for this configuration manager + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + else + { + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + + return Task.CompletedTask; + }, + + OnTokenValidated = context => + { + return Task.CompletedTask; + }, + OnForbidden = context => + { + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + return Task.CompletedTask; + } + }; + }); + } + + /// + /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// Read from the TokenValidation configuration section by . + /// + /// + /// An Enabled key may also appear in the same configuration section. When set to false, + /// authentication is disabled entirely and this class is not read. This key is not a property of + /// because it is evaluated before deserialization. + /// + public class TokenValidationOptions + { + /// + /// One or more Client IDs of the Azure Bot registration. At least one value is required. + /// + public IList? Audiences { get; set; } + + /// + /// Tenant ID of the Azure Bot. Optional but recommended. + /// When provided, tenant-specific issuer URLs are added to automatically. + /// + public string? TenantId { get; set; } + + /// + /// Override the list of trusted token issuers. Optional. + /// When omitted, default issuers are derived from and . + /// For Public cloud the defaults include the Azure Bot Service issuer and common Microsoft tenant issuers. + /// For Gov cloud the defaults include plus + /// tenant-specific issuer URLs built from + /// and . + /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// + public IList? ValidIssuers { get; set; } + + /// + /// Set to true for Azure Government (USGov) cloud deployments. Defaults to false (Public cloud). + /// When true, the following defaults are applied to any property that is not set explicitly: + /// + /// + /// → + /// + /// (https://login.botframework.azure.us/v1/.well-known/openidconfiguration) + /// + /// + /// → + /// + /// (https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration) + /// + /// + /// → + /// (https://api.botframework.us), + /// plus tenant-specific v1 and v2 issuer URLs when is provided. + /// + /// + /// For China or other sovereign clouds, leave this false and set all URLs and issuers explicitly. + /// + public bool IsGov { get; set; } = false; + + /// + /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. + /// When omitted, defaults to when + /// is false, or + /// when is true. + /// Set explicitly for China or other sovereign clouds. + /// + public string? AzureBotServiceOpenIdMetadataUrl { get; set; } + + /// + /// OpenID Connect metadata URL used to validate Entra ID (AAD) tokens. Optional. + /// When omitted, defaults to when + /// is false, or + /// when is true. + /// Set explicitly for China or other sovereign clouds. + /// + public string? OpenIdMetadataUrl { get; set; } + + /// + /// Enables special handling for tokens issued directly by Azure Bot Service (as opposed to Entra ID tokens). + /// Defaults to true and should remain true until Azure Bot Service sends Entra ID tokens exclusively. + /// When true, the endpoint is used for ABS token validation + /// and is used for all other tokens. + /// + public bool AzureBotServiceTokenHandling { get; set; } = true; + + /// + /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. + /// + public TimeSpan? OpenIdMetadataRefresh { get; set; } + } +} diff --git a/samples/dotnet/persistent-state/MyAgent.cs b/samples/dotnet/persistent-state/MyAgent.cs new file mode 100644 index 00000000..6a601496 --- /dev/null +++ b/samples/dotnet/persistent-state/MyAgent.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App; +using Microsoft.Agents.Builder.State; +using Microsoft.Agents.Core.Models; +using System.Threading; +using System.Threading.Tasks; + +namespace PersistentState; + +public class MyAgent : AgentApplication +{ + private const string MessageCountKey = "messageCount"; + + public MyAgent(AgentApplicationOptions options) : base(options) + { + OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync); + OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last); + } + + private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + foreach (ChannelAccount member in turnContext.Activity.MembersAdded) + { + if (member.Id != turnContext.Activity.Recipient.Id) + { + await turnContext.SendActivityAsync(MessageFactory.Text("Hello and Welcome!"), cancellationToken); + } + } + } + + private async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + int count = turnState.Conversation.GetValue(MessageCountKey) + 1; + turnState.Conversation.SetValue(MessageCountKey, count); + + await turnContext.SendActivityAsync( + $"[{count}] You said: {turnContext.Activity.Text}", + cancellationToken: cancellationToken); + } +} diff --git a/samples/dotnet/persistent-state/PersistentState.csproj b/samples/dotnet/persistent-state/PersistentState.csproj new file mode 100644 index 00000000..055a9cff --- /dev/null +++ b/samples/dotnet/persistent-state/PersistentState.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + latest + disable + enable + + + + + + + + + diff --git a/samples/dotnet/persistent-state/Program.cs b/samples/dotnet/persistent-state/Program.cs new file mode 100644 index 00000000..b54f39ed --- /dev/null +++ b/samples/dotnet/persistent-state/Program.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using PersistentState; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Storage; +using Microsoft.Agents.Storage.Blobs; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services.AddHttpClient(); + +// Add the AgentApplication, which contains the logic for responding to +// user messages. +builder.AddAgent(); + +// Register IStorage backed by Azure Blob Storage so that conversation state +// persists across Agent restarts and scales correctly in a cluster. +// +// Set AZURE_BLOB_STORAGE_CONNECTION_STRING in environment or user secrets. +// Set AZURE_BLOB_STORAGE_CONTAINER_NAME to override the default container name. +string connectionString = builder.Configuration["AZURE_BLOB_STORAGE_CONNECTION_STRING"] + ?? throw new InvalidOperationException( + "AZURE_BLOB_STORAGE_CONNECTION_STRING is required. " + + "Set it in environment variables or user secrets."); + +string containerName = builder.Configuration["AZURE_BLOB_STORAGE_CONTAINER_NAME"] + ?? "agents-persistent-state"; + +builder.Services.AddSingleton(new BlobsStorage(connectionString, containerName)); + +// Add AspNet token validation for Azure Bot Service and Entra. Authentication is +// configured in the appsettings.json "TokenValidation" section. +builder.Services.AddAgentAspNetAuthentication(builder.Configuration); + +WebApplication app = builder.Build(); + +// Enable AspNet authentication and authorization +app.UseAuthentication(); +app.UseAuthorization(); + +// Map GET "/" +app.MapAgentRootEndpoint(); + +// Map the endpoints for all agents using the [AgentInterface] attribute. +// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). +app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); + +app.Run(); diff --git a/samples/dotnet/persistent-state/Properties/launchSettings.json b/samples/dotnet/persistent-state/Properties/launchSettings.json new file mode 100644 index 00000000..9382ca07 --- /dev/null +++ b/samples/dotnet/persistent-state/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "PersistentState": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:3979;http://localhost:3978" + } + } +} diff --git a/samples/dotnet/persistent-state/README.md b/samples/dotnet/persistent-state/README.md new file mode 100644 index 00000000..779c2113 --- /dev/null +++ b/samples/dotnet/persistent-state/README.md @@ -0,0 +1,99 @@ +# Persistent State + +> **Sample tier:** Tier 2: Scenario starter +> **Language:** .NET +> **Scenario:** Durable conversation state backed by Azure Blob Storage +> **Estimated setup time:** 15 minutes +> **Supported channels:** Agents Playground, Web Chat, Teams, Microsoft 365 Copilot, custom app + +## What this sample demonstrates + +- How to register `IStorage` backed by Azure Blob Storage so that conversation state persists across agent restarts. +- How to read and write per-conversation state values using `ITurnState.Conversation.GetValue` / `SetValue`. +- A simple turn counter that increments on every message and survives process recycling. + +## What this sample does not demonstrate + +- Managed identity or workload identity authentication to Blob Storage (connection string only; see [Next steps](#next-steps) for the production path). +- Multi-region or geo-redundant storage configuration. +- State schema versioning or migration. +- User-scoped state (only conversation-scoped state is shown). + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- An Azure Storage account with a Blob container (or [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local emulation) +- An Azure Bot registration (required to connect a channel; not required for local emulator testing with [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview)) + +## Configure the sample + +1. Create an Azure Storage account (or start Azurite for local development): + + ```bash + # Azurite emulator — no Azure account needed for local testing + npx azurite --silent + ``` + +2. Set the connection string. Use environment variables or [.NET user secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets): + + ```bash + # Environment variable (PowerShell) + $env:AZURE_BLOB_STORAGE_CONNECTION_STRING = "" + + # Environment variable (Bash / Azurite default) + export AZURE_BLOB_STORAGE_CONNECTION_STRING="UseDevelopmentStorage=true" + + # Or via user secrets (recommended so the secret is never committed) + dotnet user-secrets set "AZURE_BLOB_STORAGE_CONNECTION_STRING" "" + ``` + +3. *(Optional)* Override the container name (must be lowercase letters, numbers, or hyphens, 3–63 characters): + + ```bash + $env:AZURE_BLOB_STORAGE_CONTAINER_NAME = "my-agent-state" + ``` + + The default container name is `agents-persistent-state`. The container is created automatically by the SDK on first use. + +4. *(When connecting a real channel)* Fill in `appsettings.json`: + - Set `TokenValidation.Enabled` to `true`. + - Replace `{{ClientId}}` and `{{TenantId}}` with your Azure Bot registration values. + - Configure `Connections.ServiceConnection.Settings` with the auth type and credentials for your bot. + +## Run locally + +```bash +cd samples/dotnet/persistent-state +dotnet run +``` + +## Verify the sample + +1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) or the Bot Framework Emulator and connect to `http://localhost:3978/api/message`. +2. Send any message. +3. Confirm: the reply reads `[1] You said: `. +4. Send another message. +5. Confirm: the counter increments — `[2] You said: `. +6. Stop and restart the agent process, then send a message. +7. Confirm: the counter continues from where it left off, demonstrating that state was persisted to Blob Storage. + +## Intentional shortcuts + +| Shortcut | Why it is acceptable in this sample | Production alternative | +|----------|-------------------------------------|------------------------| +| Connection string for storage auth | Simplest local setup; works with Azurite | [Managed identity](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity) or [workload identity](https://learn.microsoft.com/azure/aks/workload-identity-overview) — use the `BlobsStorage(Uri, TokenCredential, ...)` constructor | +| `agents-persistent-state` default container | Avoids a configuration step for quick evaluation | Choose a container name that reflects the workload and environment | +| No state schema versioning | Counter is a primitive type; schema evolution is trivial | Version state keys or use a migration strategy for complex state objects | + +## Next steps + +- **Cloud deployment with managed identity:** Replace the connection-string constructor with + `new BlobsStorage(new Uri("https://.blob.core.windows.net/"), new DefaultAzureCredential())`. + Assign the `Storage Blob Data Contributor` role to your agent's managed identity on the container. + See [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity). + +- **Workload identity on AKS:** Follow the [Workload identity overview](https://learn.microsoft.com/azure/aks/workload-identity-overview) to federate a Kubernetes service account with an Entra application and use `DefaultAzureCredential` without any secrets in the pod spec. + +- **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. + +- **Next starter sample:** [Multi-agent](../multiagent/README.md) — learn how to route conversations across multiple specialized agents. diff --git a/samples/dotnet/persistent-state/appsettings.json b/samples/dotnet/persistent-state/appsettings.json new file mode 100644 index 00000000..a3da0fee --- /dev/null +++ b/samples/dotnet/persistent-state/appsettings.json @@ -0,0 +1,39 @@ +{ + "TokenValidation": { + "Enabled": false, + "Audiences": [ + "{{ClientId}}" // this is the Client ID used for the Azure Bot + ], + "TenantId": "{{TenantId}}" + }, + + "AgentApplication": { + "StartTypingTimer": false, + "RemoveRecipientMention": false, + "NormalizeMentions": false + }, + + "Connections": { + "ServiceConnection": { + "Settings": { + // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret. + "AuthType": "" + + // Other properties dependent on the authorization type the Azure Bot uses. + } + } + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "ServiceConnection" + } + ], + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} From b8e962344d70ee52bdebb94c5dd6fc6292289443 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:59:55 -0400 Subject: [PATCH 04/33] Polish .NET persistent state starter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 5 +---- samples/dotnet/persistent-state/Program.cs | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index 377ecd93..52263c75 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -38,9 +38,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopilotSdk", "copilot-sdk\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlackAgent", "slackagent\SlackAgent.csproj", "{74277777-C4F9-2F3E-162A-02D8A8E4B702}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "persistent-state", "persistent-state", "{1A7EC4A9-7FBF-852D-259A-AE3D2D6005ED}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentState", "persistent-state\PersistentState.csproj", "{C40EE44E-A349-4B48-9556-AEA743E23092}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PersistentState", "persistent-state\PersistentState.csproj", "{C40EE44E-A349-4B48-9556-AEA743E23092}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -249,7 +247,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {C40EE44E-A349-4B48-9556-AEA743E23092} = {1A7EC4A9-7FBF-852D-259A-AE3D2D6005ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {12345678-1234-5678-9012-123456789012} diff --git a/samples/dotnet/persistent-state/Program.cs b/samples/dotnet/persistent-state/Program.cs index b54f39ed..8cf55545 100644 --- a/samples/dotnet/persistent-state/Program.cs +++ b/samples/dotnet/persistent-state/Program.cs @@ -14,10 +14,6 @@ builder.Services.AddHttpClient(); -// Add the AgentApplication, which contains the logic for responding to -// user messages. -builder.AddAgent(); - // Register IStorage backed by Azure Blob Storage so that conversation state // persists across Agent restarts and scales correctly in a cluster. // @@ -33,6 +29,10 @@ builder.Services.AddSingleton(new BlobsStorage(connectionString, containerName)); +// Add the AgentApplication, which contains the logic for responding to +// user messages. +builder.AddAgent(); + // Add AspNet token validation for Azure Bot Service and Entra. Authentication is // configured in the appsettings.json "TokenValidation" section. builder.Services.AddAgentAspNetAuthentication(builder.Configuration); From 536c1b79d24502e3d8b63a37a926e5cdfbb515d2 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:04:56 -0400 Subject: [PATCH 05/33] Clean .NET sample solution entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 134 ------------------------------------- 1 file changed, 134 deletions(-) diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index 52263c75..4fabed7a 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -43,211 +43,77 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x64.ActiveCfg = Debug|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x64.Build.0 = Debug|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x86.ActiveCfg = Debug|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Debug|x86.Build.0 = Debug|Any CPU {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|Any CPU.ActiveCfg = Release|Any CPU {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|Any CPU.Build.0 = Release|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x64.ActiveCfg = Release|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x64.Build.0 = Release|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x86.ActiveCfg = Release|Any CPU - {6547D985-7762-488D-9ADC-1BC3FD62AF66}.Release|x86.Build.0 = Release|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x64.ActiveCfg = Debug|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x64.Build.0 = Debug|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x86.ActiveCfg = Debug|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Debug|x86.Build.0 = Debug|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|Any CPU.Build.0 = Release|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x64.ActiveCfg = Release|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x64.Build.0 = Release|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x86.ActiveCfg = Release|Any CPU - {355B8CA0-6C85-4C42-B68A-2736624D0B2F}.Release|x86.Build.0 = Release|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|Any CPU.Build.0 = Debug|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x64.ActiveCfg = Debug|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x64.Build.0 = Debug|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x86.ActiveCfg = Debug|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Debug|x86.Build.0 = Debug|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|Any CPU.ActiveCfg = Release|Any CPU {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|Any CPU.Build.0 = Release|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x64.ActiveCfg = Release|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x64.Build.0 = Release|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x86.ActiveCfg = Release|Any CPU - {570A35C7-AC4D-4313-9FD9-84F2E4003832}.Release|x86.Build.0 = Release|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x64.ActiveCfg = Debug|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x64.Build.0 = Debug|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x86.ActiveCfg = Debug|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Debug|x86.Build.0 = Debug|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|Any CPU.Build.0 = Release|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x64.ActiveCfg = Release|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x64.Build.0 = Release|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x86.ActiveCfg = Release|Any CPU - {6F14E243-7ED3-43D8-A433-5BA4D52077CF}.Release|x86.Build.0 = Release|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x64.ActiveCfg = Debug|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x64.Build.0 = Debug|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x86.ActiveCfg = Debug|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Debug|x86.Build.0 = Debug|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|Any CPU.Build.0 = Release|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x64.ActiveCfg = Release|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x64.Build.0 = Release|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x86.ActiveCfg = Release|Any CPU - {9419EAC0-31D3-47DC-A008-9EDF584076D6}.Release|x86.Build.0 = Release|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x64.ActiveCfg = Debug|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x64.Build.0 = Debug|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x86.ActiveCfg = Debug|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Debug|x86.Build.0 = Debug|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|Any CPU.Build.0 = Release|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x64.ActiveCfg = Release|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x64.Build.0 = Release|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x86.ActiveCfg = Release|Any CPU - {2989FC05-935F-2F5E-5704-E3B6BFC0A3B7}.Release|x86.Build.0 = Release|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x64.ActiveCfg = Debug|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x64.Build.0 = Debug|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x86.ActiveCfg = Debug|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Debug|x86.Build.0 = Debug|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|Any CPU.Build.0 = Release|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x64.ActiveCfg = Release|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x64.Build.0 = Release|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x86.ActiveCfg = Release|Any CPU - {C326AAC3-7AA5-0DF6-3182-9650728F2F8E}.Release|x86.Build.0 = Release|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x64.ActiveCfg = Debug|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x64.Build.0 = Debug|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x86.ActiveCfg = Debug|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Debug|x86.Build.0 = Debug|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|Any CPU.Build.0 = Release|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x64.ActiveCfg = Release|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x64.Build.0 = Release|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x86.ActiveCfg = Release|Any CPU - {ED0CBBBC-AF2F-6BCB-C555-1AF802EAEA7B}.Release|x86.Build.0 = Release|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x64.ActiveCfg = Debug|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x64.Build.0 = Debug|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x86.ActiveCfg = Debug|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Debug|x86.Build.0 = Debug|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|Any CPU.ActiveCfg = Release|Any CPU {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|Any CPU.Build.0 = Release|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x64.ActiveCfg = Release|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x64.Build.0 = Release|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x86.ActiveCfg = Release|Any CPU - {812FEFD7-E6E1-D544-9367-915A49EC6FD7}.Release|x86.Build.0 = Release|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x64.ActiveCfg = Debug|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x64.Build.0 = Debug|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x86.ActiveCfg = Debug|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Debug|x86.Build.0 = Debug|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|Any CPU.Build.0 = Release|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x64.ActiveCfg = Release|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x64.Build.0 = Release|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x86.ActiveCfg = Release|Any CPU - {84EE4770-235D-45AB-9DF2-873EB8CE33B4}.Release|x86.Build.0 = Release|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x64.ActiveCfg = Debug|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x64.Build.0 = Debug|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x86.ActiveCfg = Debug|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Debug|x86.Build.0 = Debug|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|Any CPU.ActiveCfg = Release|Any CPU {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|Any CPU.Build.0 = Release|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x64.ActiveCfg = Release|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x64.Build.0 = Release|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x86.ActiveCfg = Release|Any CPU - {81CCC64D-8774-B7F0-C7DF-F037B8B3B19E}.Release|x86.Build.0 = Release|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x64.ActiveCfg = Debug|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x64.Build.0 = Debug|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x86.ActiveCfg = Debug|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Debug|x86.Build.0 = Debug|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|Any CPU.Build.0 = Release|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x64.ActiveCfg = Release|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x64.Build.0 = Release|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x86.ActiveCfg = Release|Any CPU - {2BD2BB5B-2010-5EAC-DA2B-3D9B73F14D1A}.Release|x86.Build.0 = Release|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x64.ActiveCfg = Debug|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x64.Build.0 = Debug|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x86.ActiveCfg = Debug|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Debug|x86.Build.0 = Debug|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|Any CPU.Build.0 = Release|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x64.ActiveCfg = Release|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x64.Build.0 = Release|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x86.ActiveCfg = Release|Any CPU - {1B5F12B4-F94D-63D7-1CB5-A38EFA45C890}.Release|x86.Build.0 = Release|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x64.ActiveCfg = Debug|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x64.Build.0 = Debug|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x86.ActiveCfg = Debug|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Debug|x86.Build.0 = Debug|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|Any CPU.ActiveCfg = Release|Any CPU {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|Any CPU.Build.0 = Release|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x64.ActiveCfg = Release|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x64.Build.0 = Release|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x86.ActiveCfg = Release|Any CPU - {04775FC8-6FDC-6F03-16AD-C387BDF6D156}.Release|x86.Build.0 = Release|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|Any CPU.Build.0 = Debug|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x64.ActiveCfg = Debug|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x64.Build.0 = Debug|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x86.ActiveCfg = Debug|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Debug|x86.Build.0 = Debug|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|Any CPU.ActiveCfg = Release|Any CPU {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|Any CPU.Build.0 = Release|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x64.ActiveCfg = Release|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x64.Build.0 = Release|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x86.ActiveCfg = Release|Any CPU - {74277777-C4F9-2F3E-162A-02D8A8E4B702}.Release|x86.Build.0 = Release|Any CPU {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x64.ActiveCfg = Debug|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x64.Build.0 = Debug|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x86.ActiveCfg = Debug|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|x86.Build.0 = Debug|Any CPU {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|Any CPU.ActiveCfg = Release|Any CPU {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|Any CPU.Build.0 = Release|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x64.ActiveCfg = Release|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x64.Build.0 = Release|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x86.ActiveCfg = Release|Any CPU - {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {12345678-1234-5678-9012-123456789012} EndGlobalSection From 0bc182028d56cce5a1861af25e23519f67776862 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:41:34 -0400 Subject: [PATCH 06/33] Add JavaScript persistent state starter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/nodejs/persistent-state/README.md | 93 + .../nodejs/persistent-state/package-lock.json | 3506 +++++++++++++++++ samples/nodejs/persistent-state/package.json | 30 + samples/nodejs/persistent-state/src/index.ts | 53 + samples/nodejs/persistent-state/tsconfig.json | 20 + 5 files changed, 3702 insertions(+) create mode 100644 samples/nodejs/persistent-state/README.md create mode 100644 samples/nodejs/persistent-state/package-lock.json create mode 100644 samples/nodejs/persistent-state/package.json create mode 100644 samples/nodejs/persistent-state/src/index.ts create mode 100644 samples/nodejs/persistent-state/tsconfig.json diff --git a/samples/nodejs/persistent-state/README.md b/samples/nodejs/persistent-state/README.md new file mode 100644 index 00000000..1859216e --- /dev/null +++ b/samples/nodejs/persistent-state/README.md @@ -0,0 +1,93 @@ +# Persistent State + +> **Sample tier:** Tier 2: Scenario starter +> **Language:** JavaScript +> **Scenario:** Durable conversation state backed by Azure Blob Storage +> **Estimated setup time:** 15 minutes +> **Supported channels:** Agents Playground, Web Chat, Teams, Microsoft 365 Copilot, custom app + +## What this sample demonstrates + +- How to register `IStorage` backed by Azure Blob Storage so that conversation state persists across agent restarts. +- How to read and write per-conversation state values using `TurnState`. +- A simple turn counter that increments on every message and survives process recycling. + +## What this sample does not demonstrate + +- Managed identity or workload identity authentication to Blob Storage (connection string only; see [Next steps](#next-steps) for the production path). +- Multi-region or geo-redundant storage configuration. +- State schema versioning or migration. +- User-scoped state (only conversation-scoped state is shown). + +## Prerequisites + +- [Node.js 18 or later](https://nodejs.org/) +- An Azure Storage account with a Blob container, or [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local emulation +- An Azure Bot registration (required to connect a channel; not required for local emulator testing with [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview)) + +## Configure the sample + +1. Create an Azure Storage account (or start Azurite for local development): + + ```bash + # Azurite emulator — no Azure account needed for local testing + npx azurite --silent + ``` + +2. Create a `.env` file in the `samples/nodejs/persistent-state` directory: + + ```env + # Required — Azure Storage connection string + # For Azurite (local emulator): + AZURE_BLOB_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true + + # For a real Azure Storage account: + # AZURE_BLOB_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=... + + # Optional — override the default container name (default: agents-persistent-state) + # AZURE_BLOB_STORAGE_CONTAINER_NAME=my-agent-state + ``` + + The default container name is `agents-persistent-state`. The container is created automatically by the SDK on first use. + +3. *(When connecting a real channel)* Configure authentication for your Azure Bot registration by setting the appropriate environment variables or extending the app configuration. + +## Run locally + +```bash +cd samples/nodejs/persistent-state +npm install +npm start +``` + +## Verify the sample + +1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) and connect to `http://localhost:3978/api/message`. +2. Send any message. +3. Confirm: the reply reads `[1] You said: `. +4. Send another message. +5. Confirm: the counter increments — `[2] You said: `. +6. Stop and restart the agent process, then send a message. +7. Confirm: the counter continues from where it left off, demonstrating that state was persisted to Blob Storage. + +## Intentional shortcuts + +| Shortcut | Why it is acceptable in this sample | Production alternative | +|----------|-------------------------------------|------------------------| +| Connection string for storage auth | Simplest local setup; works with Azurite | [Managed identity](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity) or [workload identity](https://learn.microsoft.com/azure/aks/workload-identity-overview) — use the `BlobsStorage(containerName, undefined, undefined, blobServiceUrl, credential)` constructor | +| `agents-persistent-state` default container | Avoids a configuration step for quick evaluation | Choose a container name that reflects the workload and environment | +| No state schema versioning | Counter is a primitive type; schema evolution is trivial | Version state keys or use a migration strategy for complex state objects | +| Local `.env` file for secrets | Keeps local setup simple; file is git-ignored | Use Azure Key Vault, environment variables injected by the platform, or a managed identity credential chain | + +## Next steps + +- **Cloud deployment with managed identity:** Replace the connection-string constructor with + `new BlobsStorage(containerName, undefined, undefined, 'https://.blob.core.windows.net', new DefaultAzureCredential())` from `@azure/identity`. + Assign the `Storage Blob Data Contributor` role to your agent's managed identity on the container. + See [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity). + +- **Workload identity on AKS:** Follow the [Workload identity overview](https://learn.microsoft.com/azure/aks/workload-identity-overview) to federate a Kubernetes service account with an Entra application and use `DefaultAzureCredential` without any secrets in the pod spec. + +- **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. + +- **Next starter sample:** [Multi-agent](../multiagent/README.md) — learn how to route conversations across multiple specialized agents. diff --git a/samples/nodejs/persistent-state/package-lock.json b/samples/nodejs/persistent-state/package-lock.json new file mode 100644 index 00000000..7e138c53 --- /dev/null +++ b/samples/nodejs/persistent-state/package-lock.json @@ -0,0 +1,3506 @@ +{ + "name": "node-persistent-state", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-persistent-state", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@microsoft/agents-hosting-express": "^1.5.1", + "@microsoft/agents-hosting-storage-blob": "^1.5.1" + }, + "devDependencies": { + "@microsoft/m365agentsplayground": "^0.2.24", + "@types/node": "^22.15.18", + "npm-run-all": "^4.1.5", + "typescript": "^5.8.3" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.2.tgz", + "integrity": "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", + "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", + "integrity": "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.1.tgz", + "integrity": "sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.2.0.tgz", + "integrity": "sha512-ge0nGzTLmEE5lg7tSCbTBrYqMGkpFQeQEtqfcKPuGJn/FPFf8Xz51uDfZsm5xpstNZGMYPhHvnYbL8OeNp/aLw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.0.6.tgz", + "integrity": "sha512-vwGXndrTkf/5Nu0xjobrFXW1AVlrbp2IrTdmJumSERfHXMsBQC+5YqIvLxCqT2+Rn+sBvzRpGaUqHCA8CKAyjg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.2.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.31.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.31.0.tgz", + "integrity": "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.3.0", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.4.0.tgz", + "integrity": "sha512-kNhJKMxQb374KOVt63CZnGIpDcrKNzJeyANLJymxE9mCJSdRGzb+Iv9oSIiCj6tNMLypr530b9ObOiA/5OvwOg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@microsoft/agents-activity": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@microsoft/agents-activity/-/agents-activity-1.5.3.tgz", + "integrity": "sha512-rEm+b8OZuLw/abVFR0pmVqjiQnbcwRlcKg9EjkDRmX7y6fbCNpnITo6luusCH4MTCFFfLYEojFuSo0gKywz4Lg==", + "license": "MIT", + "dependencies": { + "uuid": "11.1.0", + "zod": "3.25.75" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@microsoft/agents-activity/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@microsoft/agents-hosting": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@microsoft/agents-hosting/-/agents-hosting-1.5.3.tgz", + "integrity": "sha512-Ol3U3L5aeLh3haJtPsxnEoVB/RJs2KJ8AtrinkeLW1iNxNIvdH48GG9ocFlSE3OZ3u8EJ2iIN/QTr6orYERkbg==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "1.10.1", + "@azure/msal-node": "5.0.6", + "@microsoft/agents-activity": "1.5.3", + "@microsoft/agents-telemetry": "1.5.3", + "axios": "1.15.0", + "jsonwebtoken": "9.0.3", + "jwks-rsa": "4.0.1", + "object-path": "0.11.8", + "zod": "3.25.75" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@microsoft/agents-hosting-express": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@microsoft/agents-hosting-express/-/agents-hosting-express-1.5.3.tgz", + "integrity": "sha512-+xtUNDyncIfhZRBjebBF4p5s/CdFo3OXfIVLAFFB2QOHSsTnEMlXvU6p2OZi6p91UBN5K6O6tLaFry24qWpR6g==", + "license": "MIT", + "dependencies": { + "@microsoft/agents-hosting": "1.5.3", + "express": "5.2.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@microsoft/agents-hosting-storage-blob": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@microsoft/agents-hosting-storage-blob/-/agents-hosting-storage-blob-1.5.3.tgz", + "integrity": "sha512-Q3ybLtAd2L8dFv6pm+NI+rxWXaZebb1VgM9mYOlHQEAU9Okqbk0zar3gCKjBw4KuPX4qNC/KyYn1hsTXLGxeaQ==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "1.10.1", + "@azure/storage-blob": "12.31.0", + "@microsoft/agents-activity": "1.5.3", + "@microsoft/agents-hosting": "1.5.3", + "@microsoft/agents-telemetry": "1.5.3", + "zod": "3.25.75" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@microsoft/agents-telemetry": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@microsoft/agents-telemetry/-/agents-telemetry-1.5.3.tgz", + "integrity": "sha512-CO/FeoysMJ4A/eleSG2CXrOtlQPxt2r2f89OMshYCYRd9HsYOj1ktsAyddFuuCd4ojvxO5rxBmvsX4JS3JpEFA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "1", + "@opentelemetry/api-logs": ">=0.214.0 <1" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/api-logs": { + "optional": true + } + } + }, + "node_modules/@microsoft/m365agentsplayground": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@microsoft/m365agentsplayground/-/m365agentsplayground-0.2.27.tgz", + "integrity": "sha512-8MdQyAdKv1H56IjI1iGosRwS1MZatpbp+9OZg77LWOCmmlWQXMofOAU3PrbIwgBfW75oejaUlXkqnZMVQVJjDQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "bin": { + "agentsplayground": "cli.js", + "teamsapptester": "cli.js" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", + "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", + "integrity": "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anynum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", + "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-4.0.1.tgz", + "integrity": "sha512-poXwUA8S4cP9P5N8tZS3xnUDJH8WmwSGfKK9gIaRPdjLHyJtd9iX/cngX9CUIe0Caof5JhK2EbN7N5lnnaf9NA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^6.1.3", + "limiter": "^1.1.5", + "lru-memoizer": "^3.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lru-memoizer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-3.0.0.tgz", + "integrity": "sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^11.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-path": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz", + "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==", + "license": "MIT", + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strnum": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", + "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/zod": { + "version": "3.25.75", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.75.tgz", + "integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/samples/nodejs/persistent-state/package.json b/samples/nodejs/persistent-state/package.json new file mode 100644 index 00000000..47a4eda7 --- /dev/null +++ b/samples/nodejs/persistent-state/package.json @@ -0,0 +1,30 @@ +{ + "name": "node-persistent-state", + "version": "1.0.0", + "private": true, + "description": "Agents persistent state sample backed by Azure Blob Storage", + "author": "Microsoft", + "license": "MIT", + "main": "./dist/index.js", + "scripts": { + "prebuild": "npm ci", + "build": "tsc --build", + "prestart": "npm run build", + "prestart:anon": "npm run build", + "start:anon": "node ./dist/index.js", + "start": "node --env-file .env ./dist/index.js", + "test-tool": "agentsplayground", + "test": "npm-run-all -p -r start:anon test-tool" + }, + "dependencies": { + "@microsoft/agents-hosting-express": "^1.5.1", + "@microsoft/agents-hosting-storage-blob": "^1.5.1" + }, + "devDependencies": { + "@microsoft/m365agentsplayground": "^0.2.24", + "@types/node": "^22.15.18", + "npm-run-all": "^4.1.5", + "typescript": "^5.8.3" + }, + "keywords": [] +} diff --git a/samples/nodejs/persistent-state/src/index.ts b/samples/nodejs/persistent-state/src/index.ts new file mode 100644 index 00000000..d789b88b --- /dev/null +++ b/samples/nodejs/persistent-state/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { startServer } from '@microsoft/agents-hosting-express' +import { TurnState, TurnContext, AgentApplication } from '@microsoft/agents-hosting' +import { ActivityTypes } from '@microsoft/agents-activity' +import { BlobsStorage } from '@microsoft/agents-hosting-storage-blob' + +// Create custom conversation state properties. This is +// used to store conversation properties that persist across agent restarts. +interface ConversationState { + count: number; +} +type ApplicationTurnState = TurnState + +// Read Azure Blob Storage configuration from environment variables. +// Set AZURE_BLOB_STORAGE_CONNECTION_STRING in your .env file or environment. +// For local development with Azurite, use: UseDevelopmentStorage=true +const connectionString = process.env.AZURE_BLOB_STORAGE_CONNECTION_STRING +if (!connectionString) { + throw new Error( + 'AZURE_BLOB_STORAGE_CONNECTION_STRING is required. ' + + 'Set it in your .env file or environment variables. ' + + 'For local development with Azurite, use: UseDevelopmentStorage=true' + ) +} + +const containerName = process.env.AZURE_BLOB_STORAGE_CONTAINER_NAME ?? 'agents-persistent-state' + +// Register IStorage backed by Azure Blob Storage so that conversation state +// persists across Agent restarts and operates correctly in a cluster. +// The container is created automatically on first use. +const storage = new BlobsStorage(containerName, connectionString) + +const agentApp = new AgentApplication({ + storage +}) + +// Display a welcome message when members are added +agentApp.onConversationUpdate('membersAdded', async (context: TurnContext, state: ApplicationTurnState) => { + await context.sendActivity('Hello and Welcome!') +}) + +// Listen for ANY message to be received. MUST BE AFTER ANY OTHER MESSAGE HANDLERS +agentApp.onActivity(ActivityTypes.Message, async (context: TurnContext, state: ApplicationTurnState) => { + // Increment count state — persisted to Blob Storage on each turn + let count = state.conversation.count ?? 0 + state.conversation.count = ++count + + // Echo back users message with the persistent counter + await context.sendActivity(`[${count}] You said: ${context.activity.text}`) +}) + +startServer(agentApp) diff --git a/samples/nodejs/persistent-state/tsconfig.json b/samples/nodejs/persistent-state/tsconfig.json new file mode 100644 index 00000000..31a34b1d --- /dev/null +++ b/samples/nodejs/persistent-state/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "incremental": true, + "lib": ["ES2021"], + "target": "es2019", + "module": "node16", + "declaration": true, + "sourceMap": true, + "composite": true, + "strict": true, + "moduleResolution": "node16", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + } +} From f7de4c8d6a2578170c6deb9bb65929c0ed5d2b09 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:44:45 -0400 Subject: [PATCH 07/33] Fix Node.js version prerequisite and managed identity URL in README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/nodejs/persistent-state/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/nodejs/persistent-state/README.md b/samples/nodejs/persistent-state/README.md index 1859216e..f8b9f23c 100644 --- a/samples/nodejs/persistent-state/README.md +++ b/samples/nodejs/persistent-state/README.md @@ -21,7 +21,7 @@ ## Prerequisites -- [Node.js 18 or later](https://nodejs.org/) +- [Node.js 20 or later](https://nodejs.org/) - An Azure Storage account with a Blob container, or [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local emulation - An Azure Bot registration (required to connect a channel; not required for local emulator testing with [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview)) @@ -82,7 +82,7 @@ npm start ## Next steps - **Cloud deployment with managed identity:** Replace the connection-string constructor with - `new BlobsStorage(containerName, undefined, undefined, 'https://.blob.core.windows.net', new DefaultAzureCredential())` from `@azure/identity`. + `new BlobsStorage(containerName, undefined, undefined, \`https://.blob.core.windows.net/${containerName}\`, new DefaultAzureCredential())` from `@azure/identity`. Assign the `Storage Blob Data Contributor` role to your agent's managed identity on the container. See [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity). From 31f977bef91583a1cb88a563d8cbef041d32ed0c Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:12:06 -0400 Subject: [PATCH 08/33] Fix JavaScript persistent state scripts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/nodejs/persistent-state/README.md | 2 +- samples/nodejs/persistent-state/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/nodejs/persistent-state/README.md b/samples/nodejs/persistent-state/README.md index f8b9f23c..70d33522 100644 --- a/samples/nodejs/persistent-state/README.md +++ b/samples/nodejs/persistent-state/README.md @@ -21,7 +21,7 @@ ## Prerequisites -- [Node.js 20 or later](https://nodejs.org/) +- [Node.js 20.6 or later](https://nodejs.org/) - An Azure Storage account with a Blob container, or [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local emulation - An Azure Bot registration (required to connect a channel; not required for local emulator testing with [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview)) diff --git a/samples/nodejs/persistent-state/package.json b/samples/nodejs/persistent-state/package.json index 47a4eda7..f41dce99 100644 --- a/samples/nodejs/persistent-state/package.json +++ b/samples/nodejs/persistent-state/package.json @@ -14,7 +14,7 @@ "start:anon": "node ./dist/index.js", "start": "node --env-file .env ./dist/index.js", "test-tool": "agentsplayground", - "test": "npm-run-all -p -r start:anon test-tool" + "test": "npm-run-all -p -r start test-tool" }, "dependencies": { "@microsoft/agents-hosting-express": "^1.5.1", From c8059a9c5bfce6937af70ee2638e905203cebaa9 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:18:33 -0400 Subject: [PATCH 09/33] Add Python persistent state starter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/python/persistent-state/README.md | 100 ++++++++++++++++++ .../python/persistent-state/requirements.txt | 7 ++ samples/python/persistent-state/src/agent.py | 75 +++++++++++++ samples/python/persistent-state/src/main.py | 17 +++ .../persistent-state/src/start_server.py | 32 ++++++ 5 files changed, 231 insertions(+) create mode 100644 samples/python/persistent-state/README.md create mode 100644 samples/python/persistent-state/requirements.txt create mode 100644 samples/python/persistent-state/src/agent.py create mode 100644 samples/python/persistent-state/src/main.py create mode 100644 samples/python/persistent-state/src/start_server.py diff --git a/samples/python/persistent-state/README.md b/samples/python/persistent-state/README.md new file mode 100644 index 00000000..67f27f3e --- /dev/null +++ b/samples/python/persistent-state/README.md @@ -0,0 +1,100 @@ +# Persistent State + +> **Sample tier:** Tier 2: Scenario starter +> **Language:** Python +> **Scenario:** Durable conversation state backed by Azure Blob Storage +> **Estimated setup time:** 15 minutes +> **Supported channels:** Agents Playground, Web Chat, Teams, Microsoft 365 Copilot, custom app + +## What this sample demonstrates + +- How to configure `BlobStorage` backed by Azure Blob Storage so that conversation state persists across agent restarts. +- How to read and write per-conversation state values using `TurnState`. +- A simple turn counter that increments on every message and survives process recycling. + +## What this sample does not demonstrate + +- Managed identity or workload identity authentication to Blob Storage (connection string only; see [Next steps](#next-steps) for the production path). +- Multi-region or geo-redundant storage configuration. +- State schema versioning or migration. +- User-scoped state (only conversation-scoped state is shown). + +## Prerequisites + +- Python 3.11 or later +- An Azure Storage account with a Blob container, or [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local emulation +- An Azure Bot registration (required to connect a channel; not required for local emulator testing with [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview)) + +## Configure the sample + +1. Create an Azure Storage account (or start Azurite for local development): + + ```bash + # Azurite emulator — no Azure account needed for local testing + npx azurite --silent + ``` + +2. Create a `.env` file in the `samples/python/persistent-state` directory: + + ```env + # Required — Azure Storage connection string + # For Azurite (local emulator): + AZURE_BLOB_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true + + # For a real Azure Storage account: + # AZURE_BLOB_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=... + + # Optional — override the default container name (default: agents-persistent-state) + # AZURE_BLOB_STORAGE_CONTAINER_NAME=my-agent-state + ``` + + The default container name is `agents-persistent-state`. The container is created automatically by the SDK on first use. + +3. *(When connecting a real channel)* Configure authentication for your Azure Bot registration by setting the appropriate environment variables or extending the app configuration. + +## Run locally + +```bash +cd samples/python/persistent-state + +# Create and activate a virtual environment +python -m venv .venv + +# Windows +.venv\Scripts\activate + +# macOS/Linux +# source .venv/bin/activate + +pip install -r requirements.txt +python -m src.main +``` + +## Verify the sample + +1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) and connect to `http://localhost:3978/api/messages`. +2. Send any message. +3. Confirm: the reply reads `[1] you said: `. +4. Send another message. +5. Confirm: the counter increments — `[2] you said: `. +6. Stop and restart the agent process, then send a message. +7. Confirm: the counter continues from where it left off, demonstrating that state was persisted to Blob Storage. + +## Intentional shortcuts + +| Shortcut | Why it is acceptable in this sample | Production alternative | +|----------|-------------------------------------|------------------------| +| Connection string for storage auth | Simplest local setup; works with Azurite | [Managed identity](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity) or [workload identity](https://learn.microsoft.com/azure/aks/workload-identity-overview) — instantiate `BlobStorage` with a credential from `azure-identity` instead of a connection string | +| `agents-persistent-state` default container | Avoids a configuration step for quick evaluation | Choose a container name that reflects the workload and environment | +| No state schema versioning | Counter is a primitive type; schema evolution is trivial | Version state keys or use a migration strategy for complex state objects | +| Local `.env` file for secrets | Keeps local setup simple; file is git-ignored | Use Azure Key Vault, environment variables injected by the platform, or a managed identity credential chain | + +## Next steps + +- **Cloud deployment with managed identity:** Assign the `Storage Blob Data Contributor` role to your agent's managed identity on the container, then instantiate `BlobStorage` with a `DefaultAzureCredential` from the [`azure-identity`](https://pypi.org/project/azure-identity/) package instead of a connection string. See [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity). + +- **Workload identity on AKS:** Follow the [Workload identity overview](https://learn.microsoft.com/azure/aks/workload-identity-overview) to federate a Kubernetes service account with an Entra application and use `DefaultAzureCredential` without any secrets in the pod spec. + +- **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. + +- **Next starter sample:** [Multi-turn prompt](../multi-turn-prompt/README.md) — learn how to guide a user through a multi-step conversation flow. diff --git a/samples/python/persistent-state/requirements.txt b/samples/python/persistent-state/requirements.txt new file mode 100644 index 00000000..6fa02536 --- /dev/null +++ b/samples/python/persistent-state/requirements.txt @@ -0,0 +1,7 @@ +python-dotenv +aiohttp +microsoft-agents-hosting-aiohttp +microsoft-agents-hosting-core +microsoft-agents-authentication-msal +microsoft-agents-activity +microsoft-agents-storage-blob diff --git a/samples/python/persistent-state/src/agent.py b/samples/python/persistent-state/src/agent.py new file mode 100644 index 00000000..0be363ca --- /dev/null +++ b/samples/python/persistent-state/src/agent.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys +import traceback +from dotenv import load_dotenv + +from os import environ +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import ( + Authorization, + AgentApplication, + TurnState, + TurnContext, +) +from microsoft_agents.authentication.msal import MsalConnectionManager +from microsoft_agents.activity import load_configuration_from_env +from microsoft_agents.storage.blob import BlobStorage, BlobStorageConfig + +load_dotenv() +agents_sdk_config = load_configuration_from_env(environ) + +# Read Azure Blob Storage configuration from environment variables. +# Set AZURE_BLOB_STORAGE_CONNECTION_STRING in your .env file or environment. +# For local development with Azurite, use: UseDevelopmentStorage=true +_connection_string = os.environ.get("AZURE_BLOB_STORAGE_CONNECTION_STRING") +if not _connection_string: + raise RuntimeError( + "AZURE_BLOB_STORAGE_CONNECTION_STRING is required. " + "Set it in your .env file or environment variables. " + "For local development with Azurite, use: UseDevelopmentStorage=true" + ) + +_container_name = os.environ.get( + "AZURE_BLOB_STORAGE_CONTAINER_NAME", "agents-persistent-state" +) + +# Register storage backed by Azure Blob Storage so that conversation state +# persists across agent restarts and operates correctly in a cluster. +# The container is created automatically on first use. +STORAGE = BlobStorage(BlobStorageConfig( + connection_string=_connection_string, + container_name=_container_name, +)) + +CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config) +ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER) +AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config) + +AGENT_APP = AgentApplication[TurnState]( + storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config +) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, _state: TurnState): + await context.send_activity("Hello and Welcome!") + return True + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState): + # Increment count — persisted to Blob Storage on each turn + count = (state.conversation.get("count") or 0) + 1 + state.conversation["count"] = count + + await context.send_activity(f"[{count}] you said: {context.activity.text}") + + +@AGENT_APP.error +async def on_error(context: TurnContext, error: Exception): + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + traceback.print_exc() + await context.send_activity("The bot encountered an error or bug.") diff --git a/samples/python/persistent-state/src/main.py b/samples/python/persistent-state/src/main.py new file mode 100644 index 00000000..59781164 --- /dev/null +++ b/samples/python/persistent-state/src/main.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# enable logging for Microsoft Agents library +# for more information, see README.md +import logging +ms_agents_logger = logging.getLogger("microsoft_agents") +ms_agents_logger.addHandler(logging.StreamHandler()) +ms_agents_logger.setLevel(logging.INFO) + +from .agent import AGENT_APP, CONNECTION_MANAGER +from .start_server import start_server + +start_server( + agent_application=AGENT_APP, + auth_configuration=CONNECTION_MANAGER.get_default_connection_configuration(), +) diff --git a/samples/python/persistent-state/src/start_server.py b/samples/python/persistent-state/src/start_server.py new file mode 100644 index 00000000..d76b619e --- /dev/null +++ b/samples/python/persistent-state/src/start_server.py @@ -0,0 +1,32 @@ +from os import environ +from microsoft_agents.hosting.core import AgentApplication, AgentAuthConfiguration +from microsoft_agents.hosting.aiohttp import ( + start_agent_process, + jwt_authorization_middleware, + CloudAdapter, +) +from aiohttp.web import Request, Response, Application, run_app + + +def start_server( + agent_application: AgentApplication, auth_configuration: AgentAuthConfiguration +): + async def entry_point(req: Request) -> Response: + agent: AgentApplication = req.app["agent_app"] + adapter: CloudAdapter = req.app["adapter"] + return await start_agent_process( + req, + agent, + adapter, + ) + + APP = Application(middlewares=[jwt_authorization_middleware]) + APP.router.add_post("/api/messages", entry_point) + APP["agent_configuration"] = auth_configuration + APP["agent_app"] = agent_application + APP["adapter"] = agent_application.adapter + + try: + run_app(APP, host="localhost", port=environ.get("PORT", 3978)) + except Exception as error: + raise error From 8e62c5c6024a4f84fe2c05ef1b0c5705685ab733 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:10:48 -0400 Subject: [PATCH 10/33] Align Python persistent state setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/python/persistent-state/README.md | 8 +++++++- samples/python/persistent-state/env.TEMPLATE | 12 ++++++++++++ samples/python/persistent-state/src/agent.py | 5 ++--- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 samples/python/persistent-state/env.TEMPLATE diff --git a/samples/python/persistent-state/README.md b/samples/python/persistent-state/README.md index 67f27f3e..5b133759 100644 --- a/samples/python/persistent-state/README.md +++ b/samples/python/persistent-state/README.md @@ -34,7 +34,13 @@ npx azurite --silent ``` -2. Create a `.env` file in the `samples/python/persistent-state` directory: +2. Copy `env.TEMPLATE` to `.env` in the `samples/python/persistent-state` directory and fill in the values: + + ```bash + cp samples/python/persistent-state/env.TEMPLATE samples/python/persistent-state/.env + ``` + + Then edit `.env`: ```env # Required — Azure Storage connection string diff --git a/samples/python/persistent-state/env.TEMPLATE b/samples/python/persistent-state/env.TEMPLATE new file mode 100644 index 00000000..8d8ab7b2 --- /dev/null +++ b/samples/python/persistent-state/env.TEMPLATE @@ -0,0 +1,12 @@ +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET= +CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID= + +# Required — Azure Blob Storage connection string. +# For local development with Azurite: UseDevelopmentStorage=true +# For a real Azure Storage account: +# DefaultEndpointsProtocol=https;AccountName=;AccountKey=;EndpointSuffix=core.windows.net +AZURE_BLOB_STORAGE_CONNECTION_STRING= + +# Optional — override the default container name (default: agents-persistent-state) +# AZURE_BLOB_STORAGE_CONTAINER_NAME=agents-persistent-state diff --git a/samples/python/persistent-state/src/agent.py b/samples/python/persistent-state/src/agent.py index 0be363ca..252904ca 100644 --- a/samples/python/persistent-state/src/agent.py +++ b/samples/python/persistent-state/src/agent.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import os import sys import traceback from dotenv import load_dotenv @@ -24,7 +23,7 @@ # Read Azure Blob Storage configuration from environment variables. # Set AZURE_BLOB_STORAGE_CONNECTION_STRING in your .env file or environment. # For local development with Azurite, use: UseDevelopmentStorage=true -_connection_string = os.environ.get("AZURE_BLOB_STORAGE_CONNECTION_STRING") +_connection_string = environ.get("AZURE_BLOB_STORAGE_CONNECTION_STRING") if not _connection_string: raise RuntimeError( "AZURE_BLOB_STORAGE_CONNECTION_STRING is required. " @@ -32,7 +31,7 @@ "For local development with Azurite, use: UseDevelopmentStorage=true" ) -_container_name = os.environ.get( +_container_name = environ.get( "AZURE_BLOB_STORAGE_CONTAINER_NAME", "agents-persistent-state" ) From 7fdb58b91d30ffb5195ffefa7031b594cc9bfd25 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:16:53 -0400 Subject: [PATCH 11/33] Add persistent state samples to catalogs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/README.md | 3 +-- samples/dotnet/README.md | 1 + samples/nodejs/README.md | 1 + samples/python/README.md | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/README.md b/samples/README.md index 21ffaf3b..5c0393c5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -46,11 +46,10 @@ The repository currently emphasizes Tier 1 and Tier 2 samples. Before labeling a ## Starter gaps to prioritize -The current catalog has strong coverage for quickstarts, authentication, OpenTelemetry, streaming, Copilot Studio integration, and orchestration scenarios. The next starter samples that would most improve the path from "hello world" to production are: +The current catalog has strong coverage for quickstarts, authentication, OpenTelemetry, streaming, Copilot Studio integration, orchestration scenarios, and **persistent state** ([.NET](dotnet/persistent-state/README.md), [JavaScript](nodejs/persistent-state/README.md), [Python](python/persistent-state/README.md)). The next starter samples that would most improve the path from "hello world" to production are: | Gap | Why it matters | |-----|----------------| -| Persistent state starter | Shows how to replace in-memory or process-local state with durable storage. | | Configuration and secret management starter | Shows local development settings separately from cloud-hosted secret storage. | | Error handling, retry, and throttling starter | Shows realistic handling for downstream API failures and rate limits. | | Health and readiness starter | Shows the endpoints and checks needed by hosting platforms and deployment pipelines. | diff --git a/samples/dotnet/README.md b/samples/dotnet/README.md index 98b5b734..ba09904d 100644 --- a/samples/dotnet/README.md +++ b/samples/dotnet/README.md @@ -17,4 +17,5 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Proactive|Tier 2: Scenario starter|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)| |OpenTelemetry Agent|Tier 2: Scenario starter|Configures OTel tracing, metrics, and logging with OTLP export|[otel](otel/README.md)| |Agent Framework|Tier 2: Scenario starter|Weather agent built with Microsoft Agent Framework SDK|[Agent Framework](Agent%20Framework/README.md)| +|Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| |Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| diff --git a/samples/nodejs/README.md b/samples/nodejs/README.md index e574c09b..ce4b5cec 100644 --- a/samples/nodejs/README.md +++ b/samples/nodejs/README.md @@ -17,3 +17,4 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Copilot Studio Skill|Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| |Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| |OTel Agent|Tier 2: Scenario starter|Agent with OTel instrumentation |[otel](otel/README.md)| +|Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| diff --git a/samples/python/README.md b/samples/python/README.md index ab0e503c..4847bc8d 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -14,6 +14,7 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Copilot Studio Skill|Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| |OpenTelemetry|Tier 2: Scenario starter|Instrument an agent and consume telemetry via the Aspire dashboard|[opentelemetry](otel/README.md)| |Agent Framework|Tier 2: Scenario starter|Weather agent built with Microsoft Agent Framework SDK|[agent-framework](agent-framework/README.md)| +|Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| |Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| ## Important Notice - Import Changes From 0c77808cd116eb48fb01a58fbfb3996041da22ac Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:22:52 -0400 Subject: [PATCH 12/33] Fix persistent state sample documentation links - nodejs/persistent-state: fix broken Next starter link from ../multiagent/README.md to ../multi-turn-prompt/README.md - python/persistent-state: fix broken Next starter link from ../multi-turn-prompt/README.md (path does not exist) to ../semantic-kernel-multiturn/README.md - nodejs/README.md: move Persistent State row before Copilot SDK Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/nodejs/README.md | 2 +- samples/nodejs/persistent-state/README.md | 2 +- samples/python/persistent-state/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/nodejs/README.md b/samples/nodejs/README.md index ce4b5cec..ca77c577 100644 --- a/samples/nodejs/README.md +++ b/samples/nodejs/README.md @@ -15,6 +15,6 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Cards Agent|Tier 2: Scenario starter|Agent that uses rich cards to enhance conversation design |[cards](cards/README.md)| |Multi-turn Prompt |Tier 2: Scenario starter|Multi-turn dialog flow using prompts and state management |[multi-turn-prompt](multi-turn-prompt/README.md)| |Copilot Studio Skill|Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| +|Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| |Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| |OTel Agent|Tier 2: Scenario starter|Agent with OTel instrumentation |[otel](otel/README.md)| -|Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| diff --git a/samples/nodejs/persistent-state/README.md b/samples/nodejs/persistent-state/README.md index 70d33522..c91210c6 100644 --- a/samples/nodejs/persistent-state/README.md +++ b/samples/nodejs/persistent-state/README.md @@ -90,4 +90,4 @@ npm start - **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. -- **Next starter sample:** [Multi-agent](../multiagent/README.md) — learn how to route conversations across multiple specialized agents. +- **Next starter sample:** [Multi-turn prompt](../multi-turn-prompt/README.md) — learn how to guide a user through a multi-step conversation flow. diff --git a/samples/python/persistent-state/README.md b/samples/python/persistent-state/README.md index 5b133759..6096721d 100644 --- a/samples/python/persistent-state/README.md +++ b/samples/python/persistent-state/README.md @@ -103,4 +103,4 @@ python -m src.main - **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. -- **Next starter sample:** [Multi-turn prompt](../multi-turn-prompt/README.md) — learn how to guide a user through a multi-step conversation flow. +- **Next starter sample:** [Semantic Kernel integration](../semantic-kernel-multiturn/README.md) — learn how to use Semantic Kernel to manage multi-turn conversation flows. From e63ed755b9503bba05f3b771d643293bfb71a059 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:26:13 -0400 Subject: [PATCH 13/33] Point persistent state docs to advanced next samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/nodejs/persistent-state/README.md | 2 +- samples/python/persistent-state/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/nodejs/persistent-state/README.md b/samples/nodejs/persistent-state/README.md index c91210c6..238a84f7 100644 --- a/samples/nodejs/persistent-state/README.md +++ b/samples/nodejs/persistent-state/README.md @@ -90,4 +90,4 @@ npm start - **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. -- **Next starter sample:** [Multi-turn prompt](../multi-turn-prompt/README.md) — learn how to guide a user through a multi-step conversation flow. +- **Next starter sample:** [Copilot SDK](../copilot-sdk/README.md) — learn how to build more advanced agents using the Copilot SDK. diff --git a/samples/python/persistent-state/README.md b/samples/python/persistent-state/README.md index 6096721d..b879d498 100644 --- a/samples/python/persistent-state/README.md +++ b/samples/python/persistent-state/README.md @@ -103,4 +103,4 @@ python -m src.main - **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. -- **Next starter sample:** [Semantic Kernel integration](../semantic-kernel-multiturn/README.md) — learn how to use Semantic Kernel to manage multi-turn conversation flows. +- **Next starter sample:** [Copilot SDK](../copilot-sdk/README.md) — learn how to build more advanced agents using the Copilot SDK. From f07c8bc5e8d499a3e0b136c1df97354ae1e73475 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:49:00 -0400 Subject: [PATCH 14/33] Align .NET persistent state next sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/persistent-state/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/dotnet/persistent-state/README.md b/samples/dotnet/persistent-state/README.md index 779c2113..3c36fb1a 100644 --- a/samples/dotnet/persistent-state/README.md +++ b/samples/dotnet/persistent-state/README.md @@ -96,4 +96,4 @@ dotnet run - **State at scale:** Review [Azure Blob Storage scalability targets](https://learn.microsoft.com/azure/storage/common/scalability-targets-standard-account) and consider [Cosmos DB partitioned storage](https://learn.microsoft.com/azure/cosmos-db/introduction) for higher-throughput scenarios. -- **Next starter sample:** [Multi-agent](../multiagent/README.md) — learn how to route conversations across multiple specialized agents. +- **Next starter sample:** [Copilot SDK](../copilot-sdk/README.md) — learn how to build more advanced agents using the Copilot SDK. From 5353e6f03330708958b18895b7cb5c2f43122318 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:11:28 -0400 Subject: [PATCH 15/33] Address persistent state review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 1 - samples/dotnet/persistent-state/Program.cs | 2 +- samples/dotnet/persistent-state/README.md | 2 +- samples/nodejs/persistent-state/README.md | 4 +++- samples/python/persistent-state/README.md | 2 +- samples/python/persistent-state/src/agent.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index 4fabed7a..ff82106e 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.4.11702.344 diff --git a/samples/dotnet/persistent-state/Program.cs b/samples/dotnet/persistent-state/Program.cs index 8cf55545..c96ef2ce 100644 --- a/samples/dotnet/persistent-state/Program.cs +++ b/samples/dotnet/persistent-state/Program.cs @@ -47,7 +47,7 @@ app.MapAgentRootEndpoint(); // Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). +// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/messages"). app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); app.Run(); diff --git a/samples/dotnet/persistent-state/README.md b/samples/dotnet/persistent-state/README.md index 3c36fb1a..008aaf35 100644 --- a/samples/dotnet/persistent-state/README.md +++ b/samples/dotnet/persistent-state/README.md @@ -69,7 +69,7 @@ dotnet run ## Verify the sample -1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) or the Bot Framework Emulator and connect to `http://localhost:3978/api/message`. +1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) or the Bot Framework Emulator and connect to `http://localhost:3978/api/messages`. 2. Send any message. 3. Confirm: the reply reads `[1] You said: `. 4. Send another message. diff --git a/samples/nodejs/persistent-state/README.md b/samples/nodejs/persistent-state/README.md index 238a84f7..6eb9da1a 100644 --- a/samples/nodejs/persistent-state/README.md +++ b/samples/nodejs/persistent-state/README.md @@ -62,7 +62,7 @@ npm start ## Verify the sample -1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) and connect to `http://localhost:3978/api/message`. +1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) and connect to `http://localhost:3978/api/messages`. 2. Send any message. 3. Confirm: the reply reads `[1] You said: `. 4. Send another message. @@ -70,6 +70,8 @@ npm start 6. Stop and restart the agent process, then send a message. 7. Confirm: the counter continues from where it left off, demonstrating that state was persisted to Blob Storage. +`npm test` also requires the storage settings in `.env` because the test script starts the same Blob-backed agent. + ## Intentional shortcuts | Shortcut | Why it is acceptable in this sample | Production alternative | diff --git a/samples/python/persistent-state/README.md b/samples/python/persistent-state/README.md index b879d498..ef5d661d 100644 --- a/samples/python/persistent-state/README.md +++ b/samples/python/persistent-state/README.md @@ -97,7 +97,7 @@ python -m src.main ## Next steps -- **Cloud deployment with managed identity:** Assign the `Storage Blob Data Contributor` role to your agent's managed identity on the container, then instantiate `BlobStorage` with a `DefaultAzureCredential` from the [`azure-identity`](https://pypi.org/project/azure-identity/) package instead of a connection string. See [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity). +- **Cloud deployment with managed identity:** Assign the `Storage Blob Data Contributor` role to your agent's managed identity on the container, then instantiate `BlobStorage` with a `DefaultAzureCredential` from the [`azure-identity`](https://pypi.org/project/azure-identity/) package instead of a connection string; add `azure-identity` to `requirements.txt` for that path. See [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity). - **Workload identity on AKS:** Follow the [Workload identity overview](https://learn.microsoft.com/azure/aks/workload-identity-overview) to federate a Kubernetes service account with an Entra application and use `DefaultAzureCredential` without any secrets in the pod spec. diff --git a/samples/python/persistent-state/src/agent.py b/samples/python/persistent-state/src/agent.py index 252904ca..92d15cd8 100644 --- a/samples/python/persistent-state/src/agent.py +++ b/samples/python/persistent-state/src/agent.py @@ -64,7 +64,7 @@ async def on_message(context: TurnContext, state: TurnState): count = (state.conversation.get("count") or 0) + 1 state.conversation["count"] = count - await context.send_activity(f"[{count}] you said: {context.activity.text}") + await context.send_activity(f"[{count}] You said: {context.activity.text}") @AGENT_APP.error From 5f29bacf53a351c55e3586e2dc0a546e7cb90370 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:35:20 -0400 Subject: [PATCH 16/33] Align Python persistent state verification text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/python/persistent-state/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/python/persistent-state/README.md b/samples/python/persistent-state/README.md index ef5d661d..7287ff62 100644 --- a/samples/python/persistent-state/README.md +++ b/samples/python/persistent-state/README.md @@ -80,9 +80,9 @@ python -m src.main 1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) and connect to `http://localhost:3978/api/messages`. 2. Send any message. -3. Confirm: the reply reads `[1] you said: `. +3. Confirm: the reply reads `[1] You said: `. 4. Send another message. -5. Confirm: the counter increments — `[2] you said: `. +5. Confirm: the counter increments — `[2] You said: `. 6. Stop and restart the agent process, then send a message. 7. Confirm: the counter continues from where it left off, demonstrating that state was persisted to Blob Storage. From ca17306a5876f82bfb9488dde0e156f2b9896ee4 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:16:05 -0400 Subject: [PATCH 17/33] Add .NET production reference skeleton Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 13 + .../production-reference/AspNetExtensions.cs | 342 ++++++++++++++++++ .../ProductionReference.csproj | 28 ++ .../dotnet/production-reference/Program.cs | 25 ++ .../Properties/launchSettings.json | 12 + .../production-reference/appsettings.json | 36 ++ .../tests/ProductionReference.Tests.csproj | 23 ++ .../tests/SkeletonTests.cs | 14 + 8 files changed, 493 insertions(+) create mode 100644 samples/dotnet/production-reference/AspNetExtensions.cs create mode 100644 samples/dotnet/production-reference/ProductionReference.csproj create mode 100644 samples/dotnet/production-reference/Program.cs create mode 100644 samples/dotnet/production-reference/Properties/launchSettings.json create mode 100644 samples/dotnet/production-reference/appsettings.json create mode 100644 samples/dotnet/production-reference/tests/ProductionReference.Tests.csproj create mode 100644 samples/dotnet/production-reference/tests/SkeletonTests.cs diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index ff82106e..ea4d7e28 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.4.11702.344 @@ -39,6 +40,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlackAgent", "slackagent\Sl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PersistentState", "persistent-state\PersistentState.csproj", "{C40EE44E-A349-4B48-9556-AEA743E23092}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProductionReference", "production-reference\ProductionReference.csproj", "{462C6644-9FCC-486B-B4E6-FED3BBF17346}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProductionReference.Tests", "production-reference\tests\ProductionReference.Tests.csproj", "{EA9858E1-A852-4C13-B00A-351B26EF89B9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -109,6 +114,14 @@ Global {C40EE44E-A349-4B48-9556-AEA743E23092}.Debug|Any CPU.Build.0 = Debug|Any CPU {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|Any CPU.ActiveCfg = Release|Any CPU {C40EE44E-A349-4B48-9556-AEA743E23092}.Release|Any CPU.Build.0 = Release|Any CPU + {462C6644-9FCC-486B-B4E6-FED3BBF17346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {462C6644-9FCC-486B-B4E6-FED3BBF17346}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462C6644-9FCC-486B-B4E6-FED3BBF17346}.Release|Any CPU.ActiveCfg = Release|Any CPU + {462C6644-9FCC-486B-B4E6-FED3BBF17346}.Release|Any CPU.Build.0 = Release|Any CPU + {EA9858E1-A852-4C13-B00A-351B26EF89B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA9858E1-A852-4C13-B00A-351B26EF89B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA9858E1-A852-4C13-B00A-351B26EF89B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA9858E1-A852-4C13-B00A-351B26EF89B9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/dotnet/production-reference/AspNetExtensions.cs b/samples/dotnet/production-reference/AspNetExtensions.cs new file mode 100644 index 00000000..e9c0b413 --- /dev/null +++ b/samples/dotnet/production-reference/AspNetExtensions.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Validators; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens; + +public static class AspNetExtensions +{ + private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. + /// + /// The to add authentication services to. + /// The application configuration containing a section. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + /// + /// + /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and + /// all requests will be treated as unauthenticated. This is useful for local development only. + /// + /// + /// Minimum configuration for Azure Public cloud: + /// + /// "TokenValidation": { + /// "Audiences": [ "{{ClientId}}" ], + /// "TenantId": "{{TenantId}}" + /// } + /// + /// + /// + /// Minimum configuration for Azure Government cloud — add "IsGov": true: + /// + /// "TokenValidation": { + /// "Audiences": [ "{{ClientId}}" ], + /// "TenantId": "{{TenantId}}", + /// "IsGov": true + /// } + /// + /// Setting IsGov automatically selects the correct government-cloud issuer URLs and OpenID metadata + /// endpoints. See for the full list of defaults that are applied. + /// + /// + /// For China or other sovereign clouds, omit IsGov and set + /// , + /// , and + /// explicitly. + /// See for the full set of available settings. + /// + /// + public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation") + { + IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); + + if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + { + // Noop if TokenValidation section missing or disabled. + System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); + services.AddControllers(); + return; + } + + services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests using the supplied options. + /// + /// The to add authentication services to. + /// The fully populated to use. + public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) + { + AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); + services.AddControllers(); + + // Must have at least one Audience. + if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId"); + } + + // Audience values must be GUID's + foreach (var audience in validationOptions.Audiences) + { + if (!Guid.TryParse(audience, out _)) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID"); + } + } + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) + { + if (validationOptions.IsGov) + { + validationOptions.ValidIssuers = + [ + AuthenticationConstants.GovBotFrameworkTokenIssuer, + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0" + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + else + { + validationOptions.ValidIssuers = + [ + AuthenticationConstants.BotFrameworkTokenIssuer, + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, validationOptions.TenantId)); + } + } + } + + // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. + if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl)) + { + validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; + } + + // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. + if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl)) + { + validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; + } + + var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval; + + _ = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidIssuers = validationOptions.ValidIssuers, + ValidAudiences = validationOptions.Audiences, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + }; + + // Using Microsoft.IdentityModel.Validators + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + options.Events = new JwtBearerEvents + { + // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. + OnMessageReceived = context => + { + string authorizationHeader = context.Request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authorizationHeader)) + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + return Task.CompletedTask; + } + + string[] parts = authorizationHeader?.Split(' ')!; + if (parts.Length != 2 || parts[0] != "Bearer") + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + return Task.CompletedTask; + } + + // Use JsonWebToken for lightweight issuer extraction without full token parsing + JsonWebToken token = new(parts[1]); + string issuer = token.Issuer; + + if (validationOptions.AzureBotServiceTokenHandling + && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + { + // Use the Azure Bot authority for this configuration manager + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + else + { + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key => + { + return new ConfigurationManager(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + } + + return Task.CompletedTask; + }, + + OnTokenValidated = context => + { + return Task.CompletedTask; + }, + OnForbidden = context => + { + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + return Task.CompletedTask; + } + }; + }); + } + + /// + /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// Read from the TokenValidation configuration section by . + /// + /// + /// An Enabled key may also appear in the same configuration section. When set to false, + /// authentication is disabled entirely and this class is not read. This key is not a property of + /// because it is evaluated before deserialization. + /// + public class TokenValidationOptions + { + /// + /// One or more Client IDs of the Azure Bot registration. At least one value is required. + /// + public IList? Audiences { get; set; } + + /// + /// Tenant ID of the Azure Bot. Optional but recommended. + /// When provided, tenant-specific issuer URLs are added to automatically. + /// + public string? TenantId { get; set; } + + /// + /// Override the list of trusted token issuers. Optional. + /// When omitted, default issuers are derived from and . + /// For Public cloud the defaults include the Azure Bot Service issuer and common Microsoft tenant issuers. + /// For Gov cloud the defaults include plus + /// tenant-specific issuer URLs built from + /// and . + /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// + public IList? ValidIssuers { get; set; } + + /// + /// Set to true for Azure Government (USGov) cloud deployments. Defaults to false (Public cloud). + /// When true, the following defaults are applied to any property that is not set explicitly: + /// + /// + /// → + /// + /// (https://login.botframework.azure.us/v1/.well-known/openidconfiguration) + /// + /// + /// → + /// + /// (https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration) + /// + /// + /// → + /// (https://api.botframework.us), + /// plus tenant-specific v1 and v2 issuer URLs when is provided. + /// + /// + /// For China or other sovereign clouds, leave this false and set all URLs and issuers explicitly. + /// + public bool IsGov { get; set; } = false; + + /// + /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. + /// When omitted, defaults to when + /// is false, or + /// when is true. + /// Set explicitly for China or other sovereign clouds. + /// + public string? AzureBotServiceOpenIdMetadataUrl { get; set; } + + /// + /// OpenID Connect metadata URL used to validate Entra ID (AAD) tokens. Optional. + /// When omitted, defaults to when + /// is false, or + /// when is true. + /// Set explicitly for China or other sovereign clouds. + /// + public string? OpenIdMetadataUrl { get; set; } + + /// + /// Enables special handling for tokens issued directly by Azure Bot Service (as opposed to Entra ID tokens). + /// Defaults to true and should remain true until Azure Bot Service sends Entra ID tokens exclusively. + /// When true, the endpoint is used for ABS token validation + /// and is used for all other tokens. + /// + public bool AzureBotServiceTokenHandling { get; set; } = true; + + /// + /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. + /// + public TimeSpan? OpenIdMetadataRefresh { get; set; } + } +} diff --git a/samples/dotnet/production-reference/ProductionReference.csproj b/samples/dotnet/production-reference/ProductionReference.csproj new file mode 100644 index 00000000..17d1bc2f --- /dev/null +++ b/samples/dotnet/production-reference/ProductionReference.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + latest + disable + enable + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/dotnet/production-reference/Program.cs b/samples/dotnet/production-reference/Program.cs new file mode 100644 index 00000000..ac412cb0 --- /dev/null +++ b/samples/dotnet/production-reference/Program.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services.AddHttpClient(); + +// Add AspNet token validation for Azure Bot Service and Entra. Authentication is +// configured in the appsettings.json "TokenValidation" section. +builder.Services.AddAgentAspNetAuthentication(builder.Configuration); + +WebApplication app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", () => "ProductionReference is running."); + +app.Run(); + +// Required for WebApplicationFactory in tests. +public partial class Program { } \ No newline at end of file diff --git a/samples/dotnet/production-reference/Properties/launchSettings.json b/samples/dotnet/production-reference/Properties/launchSettings.json new file mode 100644 index 00000000..1b0ee853 --- /dev/null +++ b/samples/dotnet/production-reference/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ProductionReference": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:3979;http://localhost:3978" + } + } +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/appsettings.json b/samples/dotnet/production-reference/appsettings.json new file mode 100644 index 00000000..0e62adb3 --- /dev/null +++ b/samples/dotnet/production-reference/appsettings.json @@ -0,0 +1,36 @@ +{ + "TokenValidation": { + "Enabled": false, + "Audiences": [ + "{{ClientId}}" + ], + "TenantId": "{{TenantId}}" + }, + + "AgentApplication": { + "StartTypingTimer": false, + "RemoveRecipientMention": false, + "NormalizeMentions": false + }, + + "Connections": { + "ServiceConnection": { + "Settings": { + "AuthType": "" + } + } + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "ServiceConnection" + } + ], + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/tests/ProductionReference.Tests.csproj b/samples/dotnet/production-reference/tests/ProductionReference.Tests.csproj new file mode 100644 index 00000000..47ee2dce --- /dev/null +++ b/samples/dotnet/production-reference/tests/ProductionReference.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + disable + enable + false + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/dotnet/production-reference/tests/SkeletonTests.cs b/samples/dotnet/production-reference/tests/SkeletonTests.cs new file mode 100644 index 00000000..ea332480 --- /dev/null +++ b/samples/dotnet/production-reference/tests/SkeletonTests.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Skeleton test — ensures the test project compiles and is discoverable. +// Replace with real endpoint tests in Task 4. +using Xunit; + +namespace ProductionReference.Tests; + +public class SkeletonTests +{ + [Fact] + public void Placeholder_ShouldPass() { } +} \ No newline at end of file From 7e1b1687b571f70fabfa69df5e6594c4328618cd Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:12:38 -0400 Subject: [PATCH 18/33] Remove production reference solution BOM Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index ea4d7e28..7ad650f8 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.4.11702.344 From e7a8f61f820ed097bda9207a1b71a2df78f6624e Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:12:54 -0400 Subject: [PATCH 19/33] Remove production reference solution leading blank Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/Samples.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/dotnet/Samples.sln b/samples/dotnet/Samples.sln index 7ad650f8..0acf44c0 100644 --- a/samples/dotnet/Samples.sln +++ b/samples/dotnet/Samples.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.4.11702.344 From 0f5904d3a0999c604466cfa9898c556fd123d80f Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:01:54 -0400 Subject: [PATCH 20/33] Normalize production reference JSON encoding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet/production-reference/Properties/launchSettings.json | 2 +- samples/dotnet/production-reference/appsettings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/dotnet/production-reference/Properties/launchSettings.json b/samples/dotnet/production-reference/Properties/launchSettings.json index 1b0ee853..5027e4c3 100644 --- a/samples/dotnet/production-reference/Properties/launchSettings.json +++ b/samples/dotnet/production-reference/Properties/launchSettings.json @@ -1,4 +1,4 @@ -{ +{ "profiles": { "ProductionReference": { "commandName": "Project", diff --git a/samples/dotnet/production-reference/appsettings.json b/samples/dotnet/production-reference/appsettings.json index 0e62adb3..b504de4b 100644 --- a/samples/dotnet/production-reference/appsettings.json +++ b/samples/dotnet/production-reference/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "TokenValidation": { "Enabled": false, "Audiences": [ From 6b7b41ebed6833e36c67a313b3a781bcbf037310 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:54:17 -0400 Subject: [PATCH 21/33] Add production reference storage configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Configuration/StorageOptions.cs | 65 +++++++++++++++++++ .../dotnet/production-reference/Program.cs | 22 ++++++- .../Storage/AgentStorageFactory.cs | 33 ++++++++++ .../tests/StorageOptionsTests.cs | 60 +++++++++++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 samples/dotnet/production-reference/Configuration/StorageOptions.cs create mode 100644 samples/dotnet/production-reference/Storage/AgentStorageFactory.cs create mode 100644 samples/dotnet/production-reference/tests/StorageOptionsTests.cs diff --git a/samples/dotnet/production-reference/Configuration/StorageOptions.cs b/samples/dotnet/production-reference/Configuration/StorageOptions.cs new file mode 100644 index 00000000..8e1bed10 --- /dev/null +++ b/samples/dotnet/production-reference/Configuration/StorageOptions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +/// +/// Configuration options for Azure Blob Storage backing the agent state store. +/// Exactly one of or must be set. +/// +public sealed class StorageOptions +{ + /// + /// Azure Blob Storage connection string. + /// Set via the AZURE_BLOB_STORAGE_CONNECTION_STRING configuration key. + /// Mutually exclusive with . + /// + public string? ConnectionString { get; init; } + + /// + /// Absolute URI to the Azure Blob Storage container, used with managed identity. + /// Set via the AZURE_BLOB_STORAGE_CONTAINER_URI configuration key. + /// Mutually exclusive with . + /// + public Uri? ContainerUri { get; init; } + + /// + /// Name of the blob container. Only used with . + /// Set via the AZURE_BLOB_STORAGE_CONTAINER_NAME configuration key. + /// Defaults to "agents-production-reference-state". + /// + public string ContainerName { get; init; } = "agents-production-reference-state"; + + /// + /// Validates the options and throws if misconfigured. + /// + /// + /// Thrown when neither or both of and are set, + /// or when is empty while using . + /// + public void Validate() + { + bool hasConnectionString = !string.IsNullOrEmpty(ConnectionString); + bool hasContainerUri = ContainerUri is not null; + + if (!hasConnectionString && !hasContainerUri) + { + throw new InvalidOperationException( + "Storage configuration is required. Set exactly one of " + + "AZURE_BLOB_STORAGE_CONNECTION_STRING or AZURE_BLOB_STORAGE_CONTAINER_URI."); + } + + if (hasConnectionString && hasContainerUri) + { + throw new InvalidOperationException( + "AZURE_BLOB_STORAGE_CONNECTION_STRING and AZURE_BLOB_STORAGE_CONTAINER_URI " + + "are mutually exclusive. Set exactly one."); + } + + if (hasConnectionString && string.IsNullOrEmpty(ContainerName)) + { + throw new InvalidOperationException( + "AZURE_BLOB_STORAGE_CONTAINER_NAME is required when using AZURE_BLOB_STORAGE_CONNECTION_STRING."); + } + } +} diff --git a/samples/dotnet/production-reference/Program.cs b/samples/dotnet/production-reference/Program.cs index ac412cb0..3292eb2f 100644 --- a/samples/dotnet/production-reference/Program.cs +++ b/samples/dotnet/production-reference/Program.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Azure.Identity; +using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +10,22 @@ builder.Services.AddHttpClient(); +// Bind and validate storage configuration. Fails fast on startup if no valid +// storage configuration is provided; there is no in-memory fallback. +StorageOptions storageOptions = new() +{ + ConnectionString = builder.Configuration["AZURE_BLOB_STORAGE_CONNECTION_STRING"], + ContainerName = builder.Configuration["AZURE_BLOB_STORAGE_CONTAINER_NAME"] + ?? "agents-production-reference-state", + ContainerUri = builder.Configuration["AZURE_BLOB_STORAGE_CONTAINER_URI"] is string raw + ? new System.Uri(raw) + : null, +}; +storageOptions.Validate(); + +builder.Services.AddSingleton( + AgentStorageFactory.Create(storageOptions, new DefaultAzureCredential())); + // Add AspNet token validation for Azure Bot Service and Entra. Authentication is // configured in the appsettings.json "TokenValidation" section. builder.Services.AddAgentAspNetAuthentication(builder.Configuration); @@ -22,4 +40,4 @@ app.Run(); // Required for WebApplicationFactory in tests. -public partial class Program { } \ No newline at end of file +public partial class Program { } diff --git a/samples/dotnet/production-reference/Storage/AgentStorageFactory.cs b/samples/dotnet/production-reference/Storage/AgentStorageFactory.cs new file mode 100644 index 00000000..4eba7dfd --- /dev/null +++ b/samples/dotnet/production-reference/Storage/AgentStorageFactory.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; +using Microsoft.Agents.Storage; +using Microsoft.Agents.Storage.Blobs; + +/// +/// Creates an instance from validated . +/// +public static class AgentStorageFactory +{ + /// + /// Creates an backed by Azure Blob Storage. + /// + /// Validated storage options. Call before passing. + /// + /// Token credential used for managed-identity authentication when is set. + /// Ignored when is set. + /// + /// A configured instance. + public static IStorage Create(StorageOptions options, TokenCredential credential) + { + if (options.ContainerUri is not null) + { + // Managed-identity path: BlobsStorage(Uri containerUri, TokenCredential credential, ...) + return new BlobsStorage(options.ContainerUri, credential); + } + + // Connection-string path: BlobsStorage(string connectionString, string containerName, ...) + return new BlobsStorage(options.ConnectionString!, options.ContainerName); + } +} diff --git a/samples/dotnet/production-reference/tests/StorageOptionsTests.cs b/samples/dotnet/production-reference/tests/StorageOptionsTests.cs new file mode 100644 index 00000000..85a14244 --- /dev/null +++ b/samples/dotnet/production-reference/tests/StorageOptionsTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Xunit; + +namespace ProductionReference.Tests; + +public class StorageOptionsTests +{ + [Fact] + public void Validate_fails_when_neither_connection_string_nor_container_uri_is_set() + { + var options = new StorageOptions(); + + var ex = Assert.Throws((Action)(() => options.Validate())); + + Assert.Contains("AZURE_BLOB_STORAGE_CONNECTION_STRING", ex.Message); + Assert.Contains("AZURE_BLOB_STORAGE_CONTAINER_URI", ex.Message); + } + + [Fact] + public void Validate_fails_when_both_connection_string_and_container_uri_are_set() + { + var options = new StorageOptions + { + ConnectionString = "UseDevelopmentStorage=true", + ContainerUri = new Uri("https://account.blob.core.windows.net/container"), + }; + + var ex = Assert.Throws((Action)(() => options.Validate())); + + Assert.Contains("AZURE_BLOB_STORAGE_CONNECTION_STRING", ex.Message); + Assert.Contains("AZURE_BLOB_STORAGE_CONTAINER_URI", ex.Message); + } + + [Fact] + public void Validate_allows_local_connection_string_only() + { + var options = new StorageOptions + { + ConnectionString = "UseDevelopmentStorage=true", + }; + + // Should not throw. + options.Validate(); + } + + [Fact] + public void Validate_allows_managed_identity_container_uri_only() + { + var options = new StorageOptions + { + ContainerUri = new Uri("https://account.blob.core.windows.net/container"), + }; + + // Should not throw. + options.Validate(); + } +} From e6e4f9ac035b17d9eecdb09212a249a1807c8fc4 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:02:43 -0400 Subject: [PATCH 22/33] Add support triage agent behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet/production-reference/Program.cs | 16 +++- .../production-reference/SupportCaseState.cs | 29 ++++++ .../SupportCaseStateAccessor.cs | 38 ++++++++ .../SupportTriageAgent.cs | 72 ++++++++++++++ .../SupportTriageDialog.cs | 96 +++++++++++++++++++ .../tests/SkeletonTests.cs | 14 --- .../tests/SupportCaseStateAccessorTests.cs | 28 ++++++ .../tests/SupportTriageDialogTests.cs | 85 ++++++++++++++++ 8 files changed, 361 insertions(+), 17 deletions(-) create mode 100644 samples/dotnet/production-reference/SupportCaseState.cs create mode 100644 samples/dotnet/production-reference/SupportCaseStateAccessor.cs create mode 100644 samples/dotnet/production-reference/SupportTriageAgent.cs create mode 100644 samples/dotnet/production-reference/SupportTriageDialog.cs delete mode 100644 samples/dotnet/production-reference/tests/SkeletonTests.cs create mode 100644 samples/dotnet/production-reference/tests/SupportCaseStateAccessorTests.cs create mode 100644 samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs diff --git a/samples/dotnet/production-reference/Program.cs b/samples/dotnet/production-reference/Program.cs index 3292eb2f..c9017956 100644 --- a/samples/dotnet/production-reference/Program.cs +++ b/samples/dotnet/production-reference/Program.cs @@ -1,10 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Azure.Identity; +using Microsoft.Agents.Hosting.AspNetCore; using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -26,6 +28,13 @@ builder.Services.AddSingleton( AgentStorageFactory.Create(storageOptions, new DefaultAzureCredential())); +// Register support triage domain services. +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Register the agent with the Agents SDK. +builder.AddAgent(); + // Add AspNet token validation for Azure Bot Service and Entra. Authentication is // configured in the appsettings.json "TokenValidation" section. builder.Services.AddAgentAspNetAuthentication(builder.Configuration); @@ -35,9 +44,10 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("/", () => "ProductionReference is running."); +app.MapAgentRootEndpoint(); +app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); app.Run(); // Required for WebApplicationFactory in tests. -public partial class Program { } +public partial class Program { } \ No newline at end of file diff --git a/samples/dotnet/production-reference/SupportCaseState.cs b/samples/dotnet/production-reference/SupportCaseState.cs new file mode 100644 index 00000000..f8804d10 --- /dev/null +++ b/samples/dotnet/production-reference/SupportCaseState.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +/// +/// Persisted state for an in-progress or completed support triage case. +/// Stored conversation-scoped via . +/// +public sealed class SupportCaseState +{ + /// Short description of the issue reported by the user. + public string? IssueSummary { get; set; } + + /// Business impact of the issue as described by the user. + public string? Impact { get; set; } + + /// Preferred contact channel (e.g. Email, Phone). + public string? ContactPreference { get; set; } + + /// Timestamp of the most recent update to this state. + public DateTimeOffset? UpdatedAt { get; set; } + + /// Returns true when all three required fields have been collected. + public bool IsComplete => + !string.IsNullOrEmpty(IssueSummary) && + !string.IsNullOrEmpty(Impact) && + !string.IsNullOrEmpty(ContactPreference); +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/SupportCaseStateAccessor.cs b/samples/dotnet/production-reference/SupportCaseStateAccessor.cs new file mode 100644 index 00000000..7ee5a3d9 --- /dev/null +++ b/samples/dotnet/production-reference/SupportCaseStateAccessor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Builder.State; +using System; + +/// +/// Thin accessor for stored in the conversation state bag. +/// All keys are conversation-scoped; the Agents SDK manages isolation per conversation. +/// +public sealed class SupportCaseStateAccessor +{ + /// + /// Stable key used to store and retrieve from + /// . Must not change between deployments. + /// + public const string Key = "supportCase"; + + /// + /// Reads the current for this conversation. + /// Returns a new empty instance when no state has been saved yet. + /// + public SupportCaseState Get(ITurnState turnState) + { + ArgumentNullException.ThrowIfNull(turnState); + return turnState.Conversation.GetValue(Key) ?? new SupportCaseState(); + } + + /// + /// Persists into the conversation state bag. + /// + public void Set(ITurnState turnState, SupportCaseState state) + { + ArgumentNullException.ThrowIfNull(turnState); + ArgumentNullException.ThrowIfNull(state); + turnState.Conversation.SetValue(Key, state); + } +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/SupportTriageAgent.cs b/samples/dotnet/production-reference/SupportTriageAgent.cs new file mode 100644 index 00000000..76f2d612 --- /dev/null +++ b/samples/dotnet/production-reference/SupportTriageAgent.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App; +using Microsoft.Agents.Builder.State; +using Microsoft.Agents.Core.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Agent that runs the support triage flow, collecting issue details and persisting +/// them to conversation state via . +/// +public sealed class SupportTriageAgent : AgentApplication +{ + private readonly SupportCaseStateAccessor _accessor; + private readonly SupportTriageDialog _dialog; + private readonly ILogger _logger; + + public SupportTriageAgent( + AgentApplicationOptions options, + SupportCaseStateAccessor accessor, + SupportTriageDialog dialog, + ILogger logger) + : base(options) + { + _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); + _dialog = dialog ?? throw new ArgumentNullException(nameof(dialog)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeAsync); + OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last); + + OnTurnError(async (turnContext, turnState, exception, cancellationToken) => + { + _logger.LogError(exception, "Unhandled error in SupportTriageAgent"); + await turnContext.SendActivityAsync( + "I'm sorry, something went wrong. Please try again.", + cancellationToken: cancellationToken); + }); + } + + private async Task WelcomeAsync( + ITurnContext turnContext, + ITurnState turnState, + CancellationToken cancellationToken) + { + foreach (ChannelAccount member in turnContext.Activity.MembersAdded) + { + if (member.Id != turnContext.Activity.Recipient.Id) + { + await turnContext.SendActivityAsync( + MessageFactory.Text("Hello! I can help you open a support case. What issue are you experiencing?"), + cancellationToken); + } + } + } + + private async Task OnMessageAsync( + ITurnContext turnContext, + ITurnState turnState, + CancellationToken cancellationToken) + { + SupportCaseState state = _accessor.Get(turnState); + SupportTriageTurnResult result = _dialog.NextTurn(state, turnContext.Activity.Text, DateTimeOffset.UtcNow); + _accessor.Set(turnState, result.State); + await turnContext.SendActivityAsync(result.ResponseText, cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/SupportTriageDialog.cs b/samples/dotnet/production-reference/SupportTriageDialog.cs new file mode 100644 index 00000000..1fd92bb8 --- /dev/null +++ b/samples/dotnet/production-reference/SupportTriageDialog.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +/// +/// Result returned by . +/// +/// The updated support case state after processing the turn. +/// The text to send back to the user. +/// True when all required fields have been collected. +public sealed record SupportTriageTurnResult(SupportCaseState State, string ResponseText, bool IsComplete); + +/// +/// Pure, stateless dialog that drives the support triage flow. +/// Collects , , +/// and one turn at a time. +/// +public sealed class SupportTriageDialog +{ + /// + /// Advances the triage dialog by one turn. + /// + /// Current case state loaded from conversation storage. + /// Raw text from the user message; may be null or whitespace. + /// Current timestamp, stamped on any state mutation. + /// A with the updated state and reply text. + public SupportTriageTurnResult NextTurn(SupportCaseState state, string? userText, DateTimeOffset now) + { + string? input = string.IsNullOrWhiteSpace(userText) ? null : userText.Trim(); + + // Already complete — return summary for any follow-up message. + if (state.IsComplete) + { + return new SupportTriageTurnResult(state, BuildSummary(state), true); + } + + // Step 1: collect issue summary. + if (string.IsNullOrEmpty(state.IssueSummary)) + { + if (input is null) + { + return new SupportTriageTurnResult( + state, + "Please describe your issue. What problem are you experiencing?", + false); + } + + state.IssueSummary = input; + state.UpdatedAt = now; + return new SupportTriageTurnResult( + state, + "What is the impact of this issue? (e.g., High \u2013 production down, Medium \u2013 degraded, Low \u2013 minor inconvenience)", + false); + } + + // Step 2: collect impact. + if (string.IsNullOrEmpty(state.Impact)) + { + if (input is null) + { + return new SupportTriageTurnResult( + state, + "What is the impact of this issue?", + false); + } + + state.Impact = input; + state.UpdatedAt = now; + return new SupportTriageTurnResult( + state, + "How would you prefer to be contacted? (e.g., Email, Phone, Chat)", + false); + } + + // Step 3: collect contact preference. + if (input is null) + { + return new SupportTriageTurnResult( + state, + "How would you prefer to be contacted?", + false); + } + + state.ContactPreference = input; + state.UpdatedAt = now; + return new SupportTriageTurnResult(state, BuildSummary(state), true); + } + + private static string BuildSummary(SupportCaseState state) => + $"Your support case has been recorded:\n" + + $"- Issue: {state.IssueSummary}\n" + + $"- Impact: {state.Impact}\n" + + $"- Contact preference: {state.ContactPreference}\n\n" + + $"We will follow up with you shortly."; +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/tests/SkeletonTests.cs b/samples/dotnet/production-reference/tests/SkeletonTests.cs deleted file mode 100644 index ea332480..00000000 --- a/samples/dotnet/production-reference/tests/SkeletonTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Skeleton test — ensures the test project compiles and is discoverable. -// Replace with real endpoint tests in Task 4. -using Xunit; - -namespace ProductionReference.Tests; - -public class SkeletonTests -{ - [Fact] - public void Placeholder_ShouldPass() { } -} \ No newline at end of file diff --git a/samples/dotnet/production-reference/tests/SupportCaseStateAccessorTests.cs b/samples/dotnet/production-reference/tests/SupportCaseStateAccessorTests.cs new file mode 100644 index 00000000..7f76230b --- /dev/null +++ b/samples/dotnet/production-reference/tests/SupportCaseStateAccessorTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Xunit; + +namespace ProductionReference.Tests; + +/// +/// Tests for . +/// Read/write behavior against a live +/// is covered by agent integration verification. +/// +public class SupportCaseStateAccessorTests +{ + [Fact] + public void Key_is_stable_supportCase() + { + Assert.Equal("supportCase", SupportCaseStateAccessor.Key); + } + + [Fact] + public void Key_is_a_compile_time_constant() + { + // Verifies the key can be used in const expressions, ensuring it never changes at runtime. + const string expected = SupportCaseStateAccessor.Key; + Assert.Equal("supportCase", expected); + } +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs b/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs new file mode 100644 index 00000000..53cb4c93 --- /dev/null +++ b/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Xunit; + +namespace ProductionReference.Tests; + +public class SupportTriageDialogTests +{ + private static readonly SupportTriageDialog Dialog = new(); + + [Fact] + public void NextTurn_prompts_for_issue_summary_when_state_is_empty() + { + var state = new SupportCaseState(); + var now = DateTimeOffset.UtcNow; + + SupportTriageTurnResult result = Dialog.NextTurn(state, null, now); + + Assert.False(result.IsComplete); + Assert.Contains("issue", result.ResponseText, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NextTurn_captures_issue_summary_and_prompts_for_impact() + { + var state = new SupportCaseState(); + var now = DateTimeOffset.UtcNow; + + SupportTriageTurnResult result = Dialog.NextTurn(state, "Login is broken", now); + + Assert.False(result.IsComplete); + Assert.Equal("Login is broken", result.State.IssueSummary); + Assert.Contains("impact", result.ResponseText, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NextTurn_captures_impact_and_prompts_for_contact_preference() + { + var state = new SupportCaseState { IssueSummary = "Login is broken" }; + var now = DateTimeOffset.UtcNow; + + SupportTriageTurnResult result = Dialog.NextTurn(state, "High - production down", now); + + Assert.False(result.IsComplete); + Assert.Equal("High - production down", result.State.Impact); + Assert.Contains("contact", result.ResponseText, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void NextTurn_captures_contact_preference_and_returns_summary() + { + var state = new SupportCaseState + { + IssueSummary = "Login is broken", + Impact = "High - production down", + }; + var now = DateTimeOffset.UtcNow; + + SupportTriageTurnResult result = Dialog.NextTurn(state, "Email", now); + + Assert.True(result.IsComplete); + Assert.Equal("Email", result.State.ContactPreference); + Assert.Contains("Login is broken", result.ResponseText); + } + + [Fact] + public void NextTurn_keeps_completed_case_and_returns_summary_for_followup() + { + var state = new SupportCaseState + { + IssueSummary = "Login is broken", + Impact = "High - production down", + ContactPreference = "Email", + }; + var now = DateTimeOffset.UtcNow; + + SupportTriageTurnResult result = Dialog.NextTurn(state, "anything", now); + + Assert.True(result.IsComplete); + Assert.Equal("Login is broken", result.State.IssueSummary); + Assert.Contains("Login is broken", result.ResponseText); + } +} \ No newline at end of file From 2ee7455d9831b3773e5f81fb7707d5f14d1c0862 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:57:04 -0400 Subject: [PATCH 23/33] Fix dialog to not mutate input state; add immutability + whitespace tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SupportTriageDialog.cs | 42 +++++++++++++------ .../tests/SupportTriageDialogTests.cs | 19 +++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/samples/dotnet/production-reference/SupportTriageDialog.cs b/samples/dotnet/production-reference/SupportTriageDialog.cs index 1fd92bb8..2f0ec947 100644 --- a/samples/dotnet/production-reference/SupportTriageDialog.cs +++ b/samples/dotnet/production-reference/SupportTriageDialog.cs @@ -15,21 +15,22 @@ public sealed record SupportTriageTurnResult(SupportCaseState State, string Resp /// Pure, stateless dialog that drives the support triage flow. /// Collects , , /// and one turn at a time. +/// Each call returns a new ; the input is never mutated. /// public sealed class SupportTriageDialog { /// /// Advances the triage dialog by one turn. /// - /// Current case state loaded from conversation storage. + /// Current case state loaded from conversation storage. Not mutated. /// Raw text from the user message; may be null or whitespace. - /// Current timestamp, stamped on any state mutation. - /// A with the updated state and reply text. + /// Current timestamp, stamped on any state change. + /// A with a new state snapshot and reply text. public SupportTriageTurnResult NextTurn(SupportCaseState state, string? userText, DateTimeOffset now) { string? input = string.IsNullOrWhiteSpace(userText) ? null : userText.Trim(); - // Already complete — return summary for any follow-up message. + // Already complete — return the unchanged state and a summary for any follow-up message. if (state.IsComplete) { return new SupportTriageTurnResult(state, BuildSummary(state), true); @@ -46,10 +47,15 @@ public SupportTriageTurnResult NextTurn(SupportCaseState state, string? userText false); } - state.IssueSummary = input; - state.UpdatedAt = now; + var next = new SupportCaseState + { + IssueSummary = input, + Impact = state.Impact, + ContactPreference = state.ContactPreference, + UpdatedAt = now, + }; return new SupportTriageTurnResult( - state, + next, "What is the impact of this issue? (e.g., High \u2013 production down, Medium \u2013 degraded, Low \u2013 minor inconvenience)", false); } @@ -65,10 +71,15 @@ public SupportTriageTurnResult NextTurn(SupportCaseState state, string? userText false); } - state.Impact = input; - state.UpdatedAt = now; + var next = new SupportCaseState + { + IssueSummary = state.IssueSummary, + Impact = input, + ContactPreference = state.ContactPreference, + UpdatedAt = now, + }; return new SupportTriageTurnResult( - state, + next, "How would you prefer to be contacted? (e.g., Email, Phone, Chat)", false); } @@ -82,9 +93,14 @@ public SupportTriageTurnResult NextTurn(SupportCaseState state, string? userText false); } - state.ContactPreference = input; - state.UpdatedAt = now; - return new SupportTriageTurnResult(state, BuildSummary(state), true); + var completed = new SupportCaseState + { + IssueSummary = state.IssueSummary, + Impact = state.Impact, + ContactPreference = input, + UpdatedAt = now, + }; + return new SupportTriageTurnResult(completed, BuildSummary(completed), true); } private static string BuildSummary(SupportCaseState state) => diff --git a/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs b/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs index 53cb4c93..645fb144 100644 --- a/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs +++ b/samples/dotnet/production-reference/tests/SupportTriageDialogTests.cs @@ -33,6 +33,8 @@ public void NextTurn_captures_issue_summary_and_prompts_for_impact() Assert.False(result.IsComplete); Assert.Equal("Login is broken", result.State.IssueSummary); Assert.Contains("impact", result.ResponseText, StringComparison.OrdinalIgnoreCase); + // Input state must not be mutated. + Assert.Null(state.IssueSummary); } [Fact] @@ -46,6 +48,8 @@ public void NextTurn_captures_impact_and_prompts_for_contact_preference() Assert.False(result.IsComplete); Assert.Equal("High - production down", result.State.Impact); Assert.Contains("contact", result.ResponseText, StringComparison.OrdinalIgnoreCase); + // Input state must not be mutated. + Assert.Null(state.Impact); } [Fact] @@ -63,6 +67,8 @@ public void NextTurn_captures_contact_preference_and_returns_summary() Assert.True(result.IsComplete); Assert.Equal("Email", result.State.ContactPreference); Assert.Contains("Login is broken", result.ResponseText); + // Input state must not be mutated. + Assert.Null(state.ContactPreference); } [Fact] @@ -82,4 +88,17 @@ public void NextTurn_keeps_completed_case_and_returns_summary_for_followup() Assert.Equal("Login is broken", result.State.IssueSummary); Assert.Contains("Login is broken", result.ResponseText); } + + [Fact] + public void NextTurn_treats_whitespace_input_as_absent_and_reprompts_for_issue_summary() + { + var state = new SupportCaseState(); + var now = DateTimeOffset.UtcNow; + + SupportTriageTurnResult result = Dialog.NextTurn(state, " ", now); + + Assert.False(result.IsComplete); + Assert.Null(result.State.IssueSummary); + Assert.Contains("issue", result.ResponseText, StringComparison.OrdinalIgnoreCase); + } } \ No newline at end of file From 71b4d441b30e28223d99d7cd8b22991e684ce8d6 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:55:18 -0400 Subject: [PATCH 24/33] Exclude nested tests from production reference app globs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet/production-reference/ProductionReference.csproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/samples/dotnet/production-reference/ProductionReference.csproj b/samples/dotnet/production-reference/ProductionReference.csproj index 17d1bc2f..22d82c1b 100644 --- a/samples/dotnet/production-reference/ProductionReference.csproj +++ b/samples/dotnet/production-reference/ProductionReference.csproj @@ -1,10 +1,11 @@ - + net8.0 latest disable enable + $(DefaultItemExcludes);tests\** @@ -21,8 +22,4 @@ - - - - \ No newline at end of file From e7c1a23b8193ebee6d59a9c82084d30656bd17e4 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:51:06 -0400 Subject: [PATCH 25/33] Add production reference health and telemetry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../StorageConfigurationHealthCheck.cs | 36 +++++++ .../dotnet/production-reference/Program.cs | 13 ++- .../Telemetry/TelemetryExtensions.cs | 70 +++++++++++++ .../appsettings.Test.json | 3 + .../tests/HealthEndpointTests.cs | 99 +++++++++++++++++++ 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 samples/dotnet/production-reference/HealthChecks/StorageConfigurationHealthCheck.cs create mode 100644 samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs create mode 100644 samples/dotnet/production-reference/appsettings.Test.json create mode 100644 samples/dotnet/production-reference/tests/HealthEndpointTests.cs diff --git a/samples/dotnet/production-reference/HealthChecks/StorageConfigurationHealthCheck.cs b/samples/dotnet/production-reference/HealthChecks/StorageConfigurationHealthCheck.cs new file mode 100644 index 00000000..13e01839 --- /dev/null +++ b/samples/dotnet/production-reference/HealthChecks/StorageConfigurationHealthCheck.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +/// +/// Reports readiness based on whether contains a valid configuration +/// shape. Does not make any live Azure Storage calls. +/// +public sealed class StorageConfigurationHealthCheck : IHealthCheck +{ + private readonly StorageOptions _options; + + public StorageConfigurationHealthCheck(StorageOptions options) + { + _options = options; + } + + public Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + _options.Validate(); + return Task.FromResult(HealthCheckResult.Healthy("Storage configuration is valid.")); + } + catch (InvalidOperationException ex) + { + return Task.FromResult(HealthCheckResult.Unhealthy("Storage configuration is invalid.", ex)); + } + } +} diff --git a/samples/dotnet/production-reference/Program.cs b/samples/dotnet/production-reference/Program.cs index c9017956..95740912 100644 --- a/samples/dotnet/production-reference/Program.cs +++ b/samples/dotnet/production-reference/Program.cs @@ -1,16 +1,18 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Azure.Identity; using Microsoft.Agents.Hosting.AspNetCore; using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient(); +builder.ConfigureProductionReferenceTelemetry(); // Bind and validate storage configuration. Fails fast on startup if no valid // storage configuration is provided; there is no in-memory fallback. @@ -25,6 +27,7 @@ }; storageOptions.Validate(); +builder.Services.AddSingleton(storageOptions); builder.Services.AddSingleton( AgentStorageFactory.Create(storageOptions, new DefaultAzureCredential())); @@ -39,15 +42,21 @@ // configured in the appsettings.json "TokenValidation" section. builder.Services.AddAgentAspNetAuthentication(builder.Configuration); +builder.Services.AddHealthChecks() + .AddCheck("storage_configuration", tags: ["ready"]); + WebApplication app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); +app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false }); +app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") }); + app.MapAgentRootEndpoint(); app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); app.Run(); // Required for WebApplicationFactory in tests. -public partial class Program { } \ No newline at end of file +public partial class Program { } diff --git a/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs b/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs new file mode 100644 index 00000000..af5defb5 --- /dev/null +++ b/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Monitor.OpenTelemetry.AspNetCore; +using Microsoft.Agents.Core.Telemetry; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +/// +/// OpenTelemetry startup extensions for the ProductionReference application. +/// +public static class TelemetryExtensions +{ + /// + /// Configures OpenTelemetry tracing, metrics, and logging for the ProductionReference service. + /// Exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is set, and to Azure Monitor when + /// APPLICATIONINSIGHTS_CONNECTION_STRING is set. + /// + public static WebApplicationBuilder ConfigureProductionReferenceTelemetry( + this WebApplicationBuilder builder) + { + string? otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; + string? appInsightsConnStr = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + + var otel = builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource + .AddService("ProductionReference")) + .WithTracing(tracing => + { + tracing + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddSource(AgentsTelemetry.SourceName); + + if (!string.IsNullOrEmpty(otlpEndpoint)) + tracing.AddOtlpExporter(); + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddMeter(AgentsTelemetry.SourceName); + + if (!string.IsNullOrEmpty(otlpEndpoint)) + metrics.AddOtlpExporter(); + }) + .WithLogging( + logging => + { + if (!string.IsNullOrEmpty(otlpEndpoint)) + logging.AddOtlpExporter(); + }, + options => + { + options.IncludeFormattedMessage = true; + options.IncludeScopes = true; + }); + + if (!string.IsNullOrEmpty(appInsightsConnStr)) + otel.UseAzureMonitor(); + + return builder; + } +} diff --git a/samples/dotnet/production-reference/appsettings.Test.json b/samples/dotnet/production-reference/appsettings.Test.json new file mode 100644 index 00000000..cdc89aaf --- /dev/null +++ b/samples/dotnet/production-reference/appsettings.Test.json @@ -0,0 +1,3 @@ +{ + "AZURE_BLOB_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true" +} diff --git a/samples/dotnet/production-reference/tests/HealthEndpointTests.cs b/samples/dotnet/production-reference/tests/HealthEndpointTests.cs new file mode 100644 index 00000000..c4bec3ac --- /dev/null +++ b/samples/dotnet/production-reference/tests/HealthEndpointTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Xunit; + +namespace ProductionReference.Tests; + +/// +/// Integration tests for health check endpoints and unit tests for +/// configuration validation. +/// +public class HealthEndpointTests : IClassFixture +{ + /// + /// Sets the environment to "Test" so that appsettings.Test.json is loaded. + /// This provides AZURE_BLOB_STORAGE_CONNECTION_STRING before startup validation + /// runs in Program.cs, without requiring a live Azure dependency. + /// + public sealed class Factory : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Test"); + } + } + + private readonly Factory _factory; + + public HealthEndpointTests(Factory factory) + { + _factory = factory; + } + + [Fact] + public async Task LivenessEndpoint_Returns200_WithoutAzureDependencies() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/health/live"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task StorageConfigurationHealthCheck_Unhealthy_WhenNoStorageConfigured() + { + var options = new StorageOptions(); + var check = new StorageConfigurationHealthCheck(options); + var context = new HealthCheckContext + { + Registration = new HealthCheckRegistration( + "storage_configuration", check, HealthStatus.Unhealthy, null), + }; + + var result = await check.CheckHealthAsync(context, CancellationToken.None); + + Assert.Equal(HealthStatus.Unhealthy, result.Status); + } + + [Fact] + public async Task StorageConfigurationHealthCheck_Healthy_WhenConnectionStringConfigured() + { + var options = new StorageOptions { ConnectionString = "UseDevelopmentStorage=true" }; + var check = new StorageConfigurationHealthCheck(options); + var context = new HealthCheckContext + { + Registration = new HealthCheckRegistration( + "storage_configuration", check, HealthStatus.Unhealthy, null), + }; + + var result = await check.CheckHealthAsync(context, CancellationToken.None); + + Assert.Equal(HealthStatus.Healthy, result.Status); + } + + [Fact] + public async Task StorageConfigurationHealthCheck_Healthy_WhenContainerUriConfigured() + { + var options = new StorageOptions + { + ContainerUri = new Uri("https://account.blob.core.windows.net/container"), + }; + var check = new StorageConfigurationHealthCheck(options); + var context = new HealthCheckContext + { + Registration = new HealthCheckRegistration( + "storage_configuration", check, HealthStatus.Unhealthy, null), + }; + + var result = await check.CheckHealthAsync(context, CancellationToken.None); + + Assert.Equal(HealthStatus.Healthy, result.Status); + } +} From 7230496ba41a8e7310409008535bc8f9b4e3647f Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:07:40 -0400 Subject: [PATCH 26/33] Keep production reference test config out of publish output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../appsettings.Test.json | 3 -- .../tests/HealthEndpointTests.cs | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) delete mode 100644 samples/dotnet/production-reference/appsettings.Test.json diff --git a/samples/dotnet/production-reference/appsettings.Test.json b/samples/dotnet/production-reference/appsettings.Test.json deleted file mode 100644 index cdc89aaf..00000000 --- a/samples/dotnet/production-reference/appsettings.Test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "AZURE_BLOB_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true" -} diff --git a/samples/dotnet/production-reference/tests/HealthEndpointTests.cs b/samples/dotnet/production-reference/tests/HealthEndpointTests.cs index c4bec3ac..0ef88cca 100644 --- a/samples/dotnet/production-reference/tests/HealthEndpointTests.cs +++ b/samples/dotnet/production-reference/tests/HealthEndpointTests.cs @@ -5,7 +5,6 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Diagnostics.HealthChecks; using Xunit; @@ -19,15 +18,36 @@ namespace ProductionReference.Tests; public class HealthEndpointTests : IClassFixture { /// - /// Sets the environment to "Test" so that appsettings.Test.json is loaded. - /// This provides AZURE_BLOB_STORAGE_CONNECTION_STRING before startup validation - /// runs in Program.cs, without requiring a live Azure dependency. + /// Sets AZURE_BLOB_STORAGE_CONNECTION_STRING as a process environment variable + /// before the starts the app, and + /// restores the prior value on disposal. + /// + /// This is necessary because Program.cs calls storageOptions.Validate() + /// during construction + /// before any ConfigureWebHost callback can inject configuration, so the value + /// must already be visible via at + /// that point. The environment variable is scoped to the current process and is restored + /// on disposal, so it does not leak into other test classes. + /// /// public sealed class Factory : WebApplicationFactory { - protected override void ConfigureWebHost(IWebHostBuilder builder) + private const string ConnectionStringKey = "AZURE_BLOB_STORAGE_CONNECTION_STRING"; + private readonly string? _previousValue; + + public Factory() + { + _previousValue = Environment.GetEnvironmentVariable(ConnectionStringKey); + Environment.SetEnvironmentVariable(ConnectionStringKey, "UseDevelopmentStorage=true"); + } + + protected override void Dispose(bool disposing) { - builder.UseEnvironment("Test"); + base.Dispose(disposing); + if (disposing) + { + Environment.SetEnvironmentVariable(ConnectionStringKey, _previousValue); + } } } From 8fab403e26069972b5067c5e0fcdd77b568fad55 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:32:54 -0400 Subject: [PATCH 27/33] Add production reference Azure infrastructure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../production-reference/infra/main.bicep | 247 ++++++++++++++++ .../production-reference/infra/main.json | 277 ++++++++++++++++++ .../infra/main.parameters.json | 23 ++ 3 files changed, 547 insertions(+) create mode 100644 samples/dotnet/production-reference/infra/main.bicep create mode 100644 samples/dotnet/production-reference/infra/main.json create mode 100644 samples/dotnet/production-reference/infra/main.parameters.json diff --git a/samples/dotnet/production-reference/infra/main.bicep b/samples/dotnet/production-reference/infra/main.bicep new file mode 100644 index 00000000..f3752c7a --- /dev/null +++ b/samples/dotnet/production-reference/infra/main.bicep @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Production Reference — Azure infrastructure for the Agents SDK production-reference sample. +// Deploys: App Service, Storage, Key Vault, Application Insights, Log Analytics, and +// role assignments that enable the Web App managed identity to access blob storage and Key Vault. +// +// Prerequisites: an Azure Bot Service registration already exists. Pass its client ID and client +// secret via the botClientId and botClientSecret parameters. Bot Service messaging endpoint +// configuration is manual (see DEPLOYMENT.md). + +// ── Parameters ───────────────────────────────────────────────────────────────── + +@description('Azure region for all resources.') +param location string = resourceGroup().location + +@description('Short prefix used to derive resource names. Lowercase letters, digits, and hyphens; 2–8 characters.') +@minLength(2) +@maxLength(8) +param namePrefix string + +@description('Client ID (App ID) of the existing Azure Bot Service registration.') +param botClientId string + +@description('Client secret of the existing Azure Bot Service registration. Stored in Key Vault; never written to app settings in plain text.') +@secure() +param botClientSecret string + +@description('Tenant ID of the Entra directory that owns the Bot registration.') +param tenantId string + +@description('Entra authority endpoint. Defaults to the public cloud endpoint for the given tenant.') +param authorityEndpoint string = '${environment().authentication.loginEndpoint}${tenantId}' + +// ── Derived names ────────────────────────────────────────────────────────────── + +var suffix = uniqueString(resourceGroup().id, namePrefix) + +// Storage account: 3–24 chars, lowercase alphanumeric only. +var storageAccountName = take(toLower(replace('${namePrefix}${suffix}', '-', '')), 24) + +// Key Vault: 3–24 chars, alphanumeric and hyphens. +var keyVaultName = take('${namePrefix}-kv-${suffix}', 24) + +var appServicePlanName = '${namePrefix}-plan' +var webAppName = '${namePrefix}-app-${take(suffix, 8)}' +var logAnalyticsName = '${namePrefix}-law' +var appInsightsName = '${namePrefix}-ai' +var blobContainerName = 'agents-state' + +// ── Built-in role definition IDs ─────────────────────────────────────────────── + +// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/storage#storage-blob-data-contributor +var storageBlobDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + +// https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/security#key-vault-secrets-user +var keyVaultSecretsUserRoleId = '4633458b-17de-408a-b874-0445c86b69e6' + +// ── Log Analytics workspace ──────────────────────────────────────────────────── + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +// ── Application Insights ─────────────────────────────────────────────────────── + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + } +} + +// ── Storage account + blob container ────────────────────────────────────────── + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageAccountName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + supportsHttpsTrafficOnly: true + } +} + +resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { + parent: storageAccount + name: 'default' +} + +resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { + parent: blobService + name: blobContainerName + properties: { + publicAccess: 'None' + } +} + +// ── App Service plan ─────────────────────────────────────────────────────────── + +resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = { + name: appServicePlanName + location: location + sku: { + name: 'B1' + } + kind: 'linux' + properties: { + reserved: true + } +} + +// ── Web App (system-assigned managed identity) ───────────────────────────────── +// App settings are defined in a separate resource below so they can depend on +// the Key Vault secret and role assignments being in place before the app +// attempts to resolve Key Vault references at startup. + +resource webApp 'Microsoft.Web/sites@2023-01-01' = { + name: webAppName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + serverFarmId: appServicePlan.id + httpsOnly: true + siteConfig: { + linuxFxVersion: 'DOTNETCORE|8.0' + healthCheckPath: '/health/live' + alwaysOn: true + } + } +} + +// ── Key Vault ────────────────────────────────────────────────────────────────── + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: keyVaultName + location: location + properties: { + tenantId: tenant().tenantId + sku: { + family: 'A' + name: 'standard' + } + enableRbacAuthorization: true + enableSoftDelete: true + softDeleteRetentionInDays: 7 + } +} + +resource botClientSecretKvSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { + parent: keyVault + name: 'BotClientSecret' + properties: { + value: botClientSecret + } +} + +// ── Role assignment: Storage Blob Data Contributor on blob container ─────────── + +resource storageBlobDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(blobContainer.id, webApp.id, storageBlobDataContributorRoleId) + scope: blobContainer + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributorRoleId) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// ── Role assignment: Key Vault Secrets User on Key Vault ────────────────────── + +resource keyVaultSecretsUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(keyVault.id, webApp.id, keyVaultSecretsUserRoleId) + scope: keyVault + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUserRoleId) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// ── App settings ─────────────────────────────────────────────────────────────── +// Defined as a separate child resource so Bicep generates an explicit dependsOn +// on the KV secret and both role assignments. This ensures the managed identity +// has Key Vault Secrets User access before the app resolves KV references at +// startup. + +resource webAppSettings 'Microsoft.Web/sites/config@2023-01-01' = { + parent: webApp + name: 'appsettings' + dependsOn: [ + keyVaultSecretsUserRoleAssignment + storageBlobDataContributorRoleAssignment + ] + properties: { + ASPNETCORE_ENVIRONMENT: 'Production' + WEBSITE_RUN_FROM_PACKAGE: '1' + WEBSITE_ENABLE_SYNC_UPDATE_SITE: 'true' + + APPLICATIONINSIGHTS_CONNECTION_STRING: appInsights.properties.ConnectionString + + AZURE_BLOB_STORAGE_CONTAINER_URI: '${storageAccount.properties.primaryEndpoints.blob}${blobContainerName}' + + TokenValidation__Enabled: 'true' + TokenValidation__Audiences__0: botClientId + TokenValidation__TenantId: tenantId + + Connections__ServiceConnection__Settings__AuthType: 'ClientSecret' + Connections__ServiceConnection__Settings__AuthorityEndpoint: authorityEndpoint + Connections__ServiceConnection__Settings__ClientId: botClientId + Connections__ServiceConnection__Settings__ClientSecret: '@Microsoft.KeyVault(SecretUri=${botClientSecretKvSecret.properties.secretUriWithVersion})' + + ConnectionsMap__0__ServiceUrl: '*' + ConnectionsMap__0__Connection: 'ServiceConnection' + } +} + +// ── Outputs ──────────────────────────────────────────────────────────────────── + +@description('Default host name of the deployed App Service (e.g. myapp.azurewebsites.net).') +output appServiceDefaultHostName string = webApp.properties.defaultHostName + +@description('Messaging endpoint URL to register in Azure Bot Service.') +output messagingEndpoint string = 'https://${webApp.properties.defaultHostName}/api/messages' + +@description('Full URI of the blob container used for agent state storage.') +output storageContainerUri string = '${storageAccount.properties.primaryEndpoints.blob}${blobContainerName}' + +@description('Name of the deployed Key Vault.') +output keyVaultName string = keyVault.name + diff --git a/samples/dotnet/production-reference/infra/main.json b/samples/dotnet/production-reference/infra/main.json new file mode 100644 index 00000000..b0200e64 --- /dev/null +++ b/samples/dotnet/production-reference/infra/main.json @@ -0,0 +1,277 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "16667270334629655499" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region for all resources." + } + }, + "namePrefix": { + "type": "string", + "minLength": 2, + "maxLength": 8, + "metadata": { + "description": "Short prefix used to derive resource names. Lowercase letters, digits, and hyphens; 2–8 characters." + } + }, + "botClientId": { + "type": "string", + "metadata": { + "description": "Client ID (App ID) of the existing Azure Bot Service registration." + } + }, + "botClientSecret": { + "type": "securestring", + "metadata": { + "description": "Client secret of the existing Azure Bot Service registration. Stored in Key Vault; never written to app settings in plain text." + } + }, + "tenantId": { + "type": "string", + "metadata": { + "description": "Tenant ID of the Entra directory that owns the Bot registration." + } + }, + "authorityEndpoint": { + "type": "string", + "defaultValue": "[format('{0}{1}', environment().authentication.loginEndpoint, parameters('tenantId'))]", + "metadata": { + "description": "Entra authority endpoint. Defaults to the public cloud endpoint for the given tenant." + } + } + }, + "variables": { + "suffix": "[uniqueString(resourceGroup().id, parameters('namePrefix'))]", + "storageAccountName": "[take(toLower(replace(format('{0}{1}', parameters('namePrefix'), variables('suffix')), '-', '')), 24)]", + "keyVaultName": "[take(format('{0}-kv-{1}', parameters('namePrefix'), variables('suffix')), 24)]", + "appServicePlanName": "[format('{0}-plan', parameters('namePrefix'))]", + "webAppName": "[format('{0}-app-{1}', parameters('namePrefix'), take(variables('suffix'), 8))]", + "logAnalyticsName": "[format('{0}-law', parameters('namePrefix'))]", + "appInsightsName": "[format('{0}-ai', parameters('namePrefix'))]", + "blobContainerName": "agents-state", + "storageBlobDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "keyVaultSecretsUserRoleId": "4633458b-17de-408a-b874-0445c86b69e6" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[variables('logAnalyticsName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30 + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false, + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}', variables('storageAccountName'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', variables('blobContainerName'))]", + "properties": { + "publicAccess": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" + ] + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2023-01-01", + "name": "[variables('appServicePlanName')]", + "location": "[parameters('location')]", + "sku": { + "name": "B1" + }, + "kind": "linux", + "properties": { + "reserved": true + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2023-01-01", + "name": "[variables('webAppName')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", + "httpsOnly": true, + "siteConfig": { + "linuxFxVersion": "DOTNETCORE|8.0", + "healthCheckPath": "/health/live", + "alwaysOn": true + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-07-01", + "name": "[variables('keyVaultName')]", + "location": "[parameters('location')]", + "properties": { + "tenantId": "[tenant().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "enableRbacAuthorization": true, + "enableSoftDelete": true, + "softDeleteRetentionInDays": 7 + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', variables('keyVaultName'), 'BotClientSecret')]", + "properties": { + "value": "[parameters('botClientSecret')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('storageBlobDataContributorRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobDataContributorRoleId'))]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName'))]", + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "name": "[guid(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('keyVaultSecretsUserRoleId'))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('keyVaultSecretsUserRoleId'))]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ] + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2023-01-01", + "name": "[format('{0}/{1}', variables('webAppName'), 'appsettings')]", + "properties": { + "ASPNETCORE_ENVIRONMENT": "Production", + "WEBSITE_RUN_FROM_PACKAGE": "1", + "WEBSITE_ENABLE_SYNC_UPDATE_SITE": "true", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]", + "AZURE_BLOB_STORAGE_CONTAINER_URI": "[format('{0}{1}', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').primaryEndpoints.blob, variables('blobContainerName'))]", + "TokenValidation__Enabled": "true", + "TokenValidation__Audiences__0": "[parameters('botClientId')]", + "TokenValidation__TenantId": "[parameters('tenantId')]", + "Connections__ServiceConnection__Settings__AuthType": "ClientSecret", + "Connections__ServiceConnection__Settings__AuthorityEndpoint": "[parameters('authorityEndpoint')]", + "Connections__ServiceConnection__Settings__ClientId": "[parameters('botClientId')]", + "Connections__ServiceConnection__Settings__ClientSecret": "[format('@Microsoft.KeyVault(SecretUri={0})', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), 'BotClientSecret'), '2023-07-01').secretUriWithVersion)]", + "ConnectionsMap__0__ServiceUrl": "*", + "ConnectionsMap__0__Connection": "ServiceConnection" + }, + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), 'BotClientSecret')]", + "[extensionResourceId(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('keyVaultSecretsUserRoleId')))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('storageBlobDataContributorRoleId')))]", + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ] + } + ], + "outputs": { + "appServiceDefaultHostName": { + "type": "string", + "metadata": { + "description": "Default host name of the deployed App Service (e.g. myapp.azurewebsites.net)." + }, + "value": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01').defaultHostName]" + }, + "messagingEndpoint": { + "type": "string", + "metadata": { + "description": "Messaging endpoint URL to register in Azure Bot Service." + }, + "value": "[format('https://{0}/api/messages', reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01').defaultHostName)]" + }, + "storageContainerUri": { + "type": "string", + "metadata": { + "description": "Full URI of the blob container used for agent state storage." + }, + "value": "[format('{0}{1}', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').primaryEndpoints.blob, variables('blobContainerName'))]" + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Key Vault." + }, + "value": "[variables('keyVaultName')]" + } + } +} \ No newline at end of file diff --git a/samples/dotnet/production-reference/infra/main.parameters.json b/samples/dotnet/production-reference/infra/main.parameters.json new file mode 100644 index 00000000..06551e4a --- /dev/null +++ b/samples/dotnet/production-reference/infra/main.parameters.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "namePrefix": { + "value": "mybot" + }, + "botClientId": { + "value": "" + }, + "botClientSecret": { + "reference": { + "keyVault": { + "id": "/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/" + }, + "secretName": "" + } + }, + "tenantId": { + "value": "" + } + } +} From 40244b294b66c4e3513c4e22ee20794afe060a9e Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:01:36 -0400 Subject: [PATCH 28/33] Remove generated production reference ARM artifact Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../production-reference/infra/main.json | 277 ------------------ 1 file changed, 277 deletions(-) delete mode 100644 samples/dotnet/production-reference/infra/main.json diff --git a/samples/dotnet/production-reference/infra/main.json b/samples/dotnet/production-reference/infra/main.json deleted file mode 100644 index b0200e64..00000000 --- a/samples/dotnet/production-reference/infra/main.json +++ /dev/null @@ -1,277 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.44.1.10279", - "templateHash": "16667270334629655499" - } - }, - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Azure region for all resources." - } - }, - "namePrefix": { - "type": "string", - "minLength": 2, - "maxLength": 8, - "metadata": { - "description": "Short prefix used to derive resource names. Lowercase letters, digits, and hyphens; 2–8 characters." - } - }, - "botClientId": { - "type": "string", - "metadata": { - "description": "Client ID (App ID) of the existing Azure Bot Service registration." - } - }, - "botClientSecret": { - "type": "securestring", - "metadata": { - "description": "Client secret of the existing Azure Bot Service registration. Stored in Key Vault; never written to app settings in plain text." - } - }, - "tenantId": { - "type": "string", - "metadata": { - "description": "Tenant ID of the Entra directory that owns the Bot registration." - } - }, - "authorityEndpoint": { - "type": "string", - "defaultValue": "[format('{0}{1}', environment().authentication.loginEndpoint, parameters('tenantId'))]", - "metadata": { - "description": "Entra authority endpoint. Defaults to the public cloud endpoint for the given tenant." - } - } - }, - "variables": { - "suffix": "[uniqueString(resourceGroup().id, parameters('namePrefix'))]", - "storageAccountName": "[take(toLower(replace(format('{0}{1}', parameters('namePrefix'), variables('suffix')), '-', '')), 24)]", - "keyVaultName": "[take(format('{0}-kv-{1}', parameters('namePrefix'), variables('suffix')), 24)]", - "appServicePlanName": "[format('{0}-plan', parameters('namePrefix'))]", - "webAppName": "[format('{0}-app-{1}', parameters('namePrefix'), take(variables('suffix'), 8))]", - "logAnalyticsName": "[format('{0}-law', parameters('namePrefix'))]", - "appInsightsName": "[format('{0}-ai', parameters('namePrefix'))]", - "blobContainerName": "agents-state", - "storageBlobDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", - "keyVaultSecretsUserRoleId": "4633458b-17de-408a-b874-0445c86b69e6" - }, - "resources": [ - { - "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2022-10-01", - "name": "[variables('logAnalyticsName')]", - "location": "[parameters('location')]", - "properties": { - "sku": { - "name": "PerGB2018" - }, - "retentionInDays": 30 - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "name": "[variables('appInsightsName')]", - "location": "[parameters('location')]", - "kind": "web", - "properties": { - "Application_Type": "web", - "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - }, - "dependsOn": [ - "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]" - ] - }, - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2023-01-01", - "name": "[variables('storageAccountName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Standard_LRS" - }, - "kind": "StorageV2", - "properties": { - "minimumTlsVersion": "TLS1_2", - "allowBlobPublicAccess": false, - "supportsHttpsTrafficOnly": true - } - }, - { - "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2023-01-01", - "name": "[format('{0}/{1}', variables('storageAccountName'), 'default')]", - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" - ] - }, - { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2023-01-01", - "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', variables('blobContainerName'))]", - "properties": { - "publicAccess": "None" - }, - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" - ] - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2023-01-01", - "name": "[variables('appServicePlanName')]", - "location": "[parameters('location')]", - "sku": { - "name": "B1" - }, - "kind": "linux", - "properties": { - "reserved": true - } - }, - { - "type": "Microsoft.Web/sites", - "apiVersion": "2023-01-01", - "name": "[variables('webAppName')]", - "location": "[parameters('location')]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]", - "httpsOnly": true, - "siteConfig": { - "linuxFxVersion": "DOTNETCORE|8.0", - "healthCheckPath": "/health/live", - "alwaysOn": true - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2023-07-01", - "name": "[variables('keyVaultName')]", - "location": "[parameters('location')]", - "properties": { - "tenantId": "[tenant().tenantId]", - "sku": { - "family": "A", - "name": "standard" - }, - "enableRbacAuthorization": true, - "enableSoftDelete": true, - "softDeleteRetentionInDays": 7 - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2023-07-01", - "name": "[format('{0}/{1}', variables('keyVaultName'), 'BotClientSecret')]", - "properties": { - "value": "[parameters('botClientSecret')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" - ] - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName'))]", - "name": "[guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('storageBlobDataContributorRoleId'))]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobDataContributorRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01', 'full').identity.principalId]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName'))]", - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ] - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", - "name": "[guid(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('keyVaultSecretsUserRoleId'))]", - "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('keyVaultSecretsUserRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01', 'full').identity.principalId]", - "principalType": "ServicePrincipal" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ] - }, - { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2023-01-01", - "name": "[format('{0}/{1}', variables('webAppName'), 'appsettings')]", - "properties": { - "ASPNETCORE_ENVIRONMENT": "Production", - "WEBSITE_RUN_FROM_PACKAGE": "1", - "WEBSITE_ENABLE_SYNC_UPDATE_SITE": "true", - "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]", - "AZURE_BLOB_STORAGE_CONTAINER_URI": "[format('{0}{1}', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').primaryEndpoints.blob, variables('blobContainerName'))]", - "TokenValidation__Enabled": "true", - "TokenValidation__Audiences__0": "[parameters('botClientId')]", - "TokenValidation__TenantId": "[parameters('tenantId')]", - "Connections__ServiceConnection__Settings__AuthType": "ClientSecret", - "Connections__ServiceConnection__Settings__AuthorityEndpoint": "[parameters('authorityEndpoint')]", - "Connections__ServiceConnection__Settings__ClientId": "[parameters('botClientId')]", - "Connections__ServiceConnection__Settings__ClientSecret": "[format('@Microsoft.KeyVault(SecretUri={0})', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), 'BotClientSecret'), '2023-07-01').secretUriWithVersion)]", - "ConnectionsMap__0__ServiceUrl": "*", - "ConnectionsMap__0__Connection": "ServiceConnection" - }, - "dependsOn": [ - "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]", - "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), 'BotClientSecret')]", - "[extensionResourceId(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('keyVaultSecretsUserRoleId')))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "[extensionResourceId(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', variables('blobContainerName')), resourceId('Microsoft.Web/sites', variables('webAppName')), variables('storageBlobDataContributorRoleId')))]", - "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" - ] - } - ], - "outputs": { - "appServiceDefaultHostName": { - "type": "string", - "metadata": { - "description": "Default host name of the deployed App Service (e.g. myapp.azurewebsites.net)." - }, - "value": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01').defaultHostName]" - }, - "messagingEndpoint": { - "type": "string", - "metadata": { - "description": "Messaging endpoint URL to register in Azure Bot Service." - }, - "value": "[format('https://{0}/api/messages', reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2023-01-01').defaultHostName)]" - }, - "storageContainerUri": { - "type": "string", - "metadata": { - "description": "Full URI of the blob container used for agent state storage." - }, - "value": "[format('{0}{1}', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').primaryEndpoints.blob, variables('blobContainerName'))]" - }, - "keyVaultName": { - "type": "string", - "metadata": { - "description": "Name of the deployed Key Vault." - }, - "value": "[variables('keyVaultName')]" - } - } -} \ No newline at end of file From bb64d8156dc9dd23383a81e45dad76a5d9bdeaea Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:31:16 -0400 Subject: [PATCH 29/33] Document .NET production reference sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/README.md | 7 +- samples/dotnet/README.md | 1 + .../dotnet/production-reference/DEPLOYMENT.md | 194 +++++++++++++++ samples/dotnet/production-reference/README.md | 196 +++++++++++++++ .../dotnet/production-reference/RUNBOOK.md | 227 ++++++++++++++++++ 5 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 samples/dotnet/production-reference/DEPLOYMENT.md create mode 100644 samples/dotnet/production-reference/README.md create mode 100644 samples/dotnet/production-reference/RUNBOOK.md diff --git a/samples/README.md b/samples/README.md index 5c0393c5..8039c15e 100644 --- a/samples/README.md +++ b/samples/README.md @@ -32,7 +32,12 @@ Use [SAMPLE_README_TEMPLATE.md](SAMPLE_README_TEMPLATE.md) when adding or refres ## Current production reference coverage -The repository currently emphasizes Tier 1 and Tier 2 samples. Before labeling a sample as Tier 3, it should include or link to: +The repository includes one Tier 3 production reference sample: + +- **.NET support triage agent** ([dotnet/production-reference](dotnet/production-reference/README.md)) — a deployable, stateful support triage agent with managed identity, durable Blob Storage state, health checks, OpenTelemetry, Bicep infrastructure, xUnit tests, and a runbook. + + +Before labeling a sample as Tier 3, it should include or link to: | Capability | Expected production-reference coverage | |------------|----------------------------------------| diff --git a/samples/dotnet/README.md b/samples/dotnet/README.md index ba09904d..07fe52a6 100644 --- a/samples/dotnet/README.md +++ b/samples/dotnet/README.md @@ -19,3 +19,4 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Agent Framework|Tier 2: Scenario starter|Weather agent built with Microsoft Agent Framework SDK|[Agent Framework](Agent%20Framework/README.md)| |Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| |Copilot SDK|Tier 2: Scenario starter|Dungeon Scribe RPG agent powered by the GitHub Copilot SDK|[copilot-sdk](copilot-sdk/README.md)| +|Production Reference|Tier 3: Production reference|Deployable support triage agent with managed identity, durable state, health checks, telemetry, Bicep, tests, and runbook|[production-reference](production-reference/README.md)| diff --git a/samples/dotnet/production-reference/DEPLOYMENT.md b/samples/dotnet/production-reference/DEPLOYMENT.md new file mode 100644 index 00000000..bddceb18 --- /dev/null +++ b/samples/dotnet/production-reference/DEPLOYMENT.md @@ -0,0 +1,194 @@ +# Deployment Guide — Production Reference Sample + +This guide covers deploying the production-reference support triage agent to Azure using the Bicep template in `infra/main.bicep`. + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) version 2.50 or later +- Bicep CLI: `az bicep install` +- An active Azure subscription: `az account show` +- An existing **Azure Bot Service registration** with a client ID and client secret. See [Create a bot resource](https://learn.microsoft.com/azure/bot-service/abs-quickstart) if you need to create one. +- The bot client secret must be available to pass as a deployment parameter. Do **not** hard-code it in source files. + +## Deployment overview + +The Bicep template provisions: + +| Resource | Purpose | +|----------|---------| +| App Service plan (B1, Linux) | Hosting plan for the .NET 8 web app | +| App Service (system-assigned managed identity) | Runs the agent; health check path `/health/live`; HTTPS only | +| Storage account + blob container (`agents-state`) | Durable conversation state | +| Key Vault | Stores `BotClientSecret`; RBAC authorization enabled | +| Log Analytics workspace | Telemetry backend | +| Application Insights | Connected to Log Analytics; connection string written to app settings | +| Role assignment: Storage Blob Data Contributor | Grants the web app managed identity read/write access to the blob container | +| Role assignment: Key Vault Secrets User | Grants the web app managed identity read access to Key Vault secrets | +| App settings | Wires together storage URI, token validation, connection settings, and Application Insights | + +Bot Service registration is **not** created by Bicep. Configure the messaging endpoint manually after deploy (see step 5 below). + +## Step 1: Log in to Azure + +```bash +az login +az account set --subscription "" +``` + +## Step 2: Create a resource group + +```bash +az group create --name "" --location "" +# Example: az group create --name prod-ref-rg --location eastus +``` + +## Step 3: Build the application package + +```bash +dotnet publish samples/dotnet/production-reference/ProductionReference.csproj \ + --configuration Release \ + --output ./publish-output +``` + +```powershell +# PowerShell equivalent +dotnet publish samples\dotnet\production-reference\ProductionReference.csproj ` + --configuration Release ` + --output .\publish-output +``` + +Compress the output for deployment (App Service `WEBSITE_RUN_FROM_PACKAGE`): + +```bash +cd publish-output && zip -r ../deploy.zip . && cd .. +``` + +```powershell +Compress-Archive -Path .\publish-output\* -DestinationPath .\deploy.zip -Force +``` + +## Step 4: Deploy the Bicep template + +### Providing `botClientSecret` securely + +The `botClientSecret` parameter is declared `@secure()` in the Bicep template. Azure Resource Manager does not log secure parameter values. + +**Option A — interactive prompt (development or one-off deploy):** + +```bash +az deployment group create \ + --resource-group "" \ + --template-file samples/dotnet/production-reference/infra/main.bicep \ + --parameters \ + namePrefix="" \ + botClientId="" \ + botClientSecret="" \ + tenantId="" +``` + +**Option B — parameters file with a Key Vault reference (recommended for automation):** + +Copy and edit `infra/main.parameters.json`, substituting your subscription ID, resource group, and Key Vault name. Then run: + +```bash +az deployment group create \ + --resource-group "" \ + --template-file samples/dotnet/production-reference/infra/main.bicep \ + --parameters @samples/dotnet/production-reference/infra/main.parameters.json +``` + +The parameters file uses `"reference": { "keyVault": {...}, "secretName": "..." }` to pull `botClientSecret` from an existing Key Vault at deploy time without the value ever appearing in shell history or CI logs. + +## Step 5: Capture deployment outputs + +```bash +az deployment group show \ + --resource-group "" \ + --name "" \ + --query properties.outputs +``` + +| Output | Description | +|--------|-------------| +| `appServiceDefaultHostName` | Hostname of the deployed App Service (e.g. `mybot-app-xxxx.azurewebsites.net`) | +| `messagingEndpoint` | Full messaging endpoint URL to register in Azure Bot Service | +| `storageContainerUri` | URI of the blob container used for agent state | +| `keyVaultName` | Name of the provisioned Key Vault | + +## Step 6: Configure the Bot Service messaging endpoint + +In the [Azure Portal](https://portal.azure.com), navigate to your existing Azure Bot resource: + +1. Open **Configuration**. +2. Set **Messaging endpoint** to the `messagingEndpoint` output value (e.g. `https://mybot-app-xxxx.azurewebsites.net/api/messages`). +3. Save. + +## Step 7: Deploy the application package + +```bash +az webapp deploy \ + --resource-group "" \ + --name "" \ + --src-path deploy.zip \ + --type zip +``` + +## Managed identity role assignments + +Both role assignments are provisioned by the Bicep `webAppSettings` resource, which `dependsOn` the role assignments. This sequencing ensures that the managed identity has the required access before the App Service attempts to resolve the Key Vault reference at startup. + +| Role | Scope | Why | +|------|-------|-----| +| `Storage Blob Data Contributor` | Blob container | Allows `BlobsStorage` (via `DefaultAzureCredential`) to read and write state blobs | +| `Key Vault Secrets User` | Key Vault | Allows App Service Key Vault reference resolution for `BotClientSecret` at app startup | + +If role propagation is slow, the first app startup may fail to resolve the Key Vault reference. Wait 30–60 seconds and restart the app, or check the RUNBOOK for remediation steps. + +## App Service settings and Key Vault reference behavior + +The Bicep template writes the following App Service application settings: + +| Setting | Value | +|---------|-------| +| `ASPNETCORE_ENVIRONMENT` | `Production` | +| `WEBSITE_RUN_FROM_PACKAGE` | `1` | +| `APPLICATIONINSIGHTS_CONNECTION_STRING` | From Application Insights resource | +| `AZURE_BLOB_STORAGE_CONTAINER_URI` | URI of the provisioned blob container | +| `TokenValidation__Enabled` | `true` | +| `TokenValidation__Audiences__0` | Bot client ID | +| `TokenValidation__TenantId` | Entra tenant ID | +| `Connections__ServiceConnection__Settings__AuthType` | `ClientSecret` | +| `Connections__ServiceConnection__Settings__AuthorityEndpoint` | Entra authority URL | +| `Connections__ServiceConnection__Settings__ClientId` | Bot client ID | +| `Connections__ServiceConnection__Settings__ClientSecret` | `@Microsoft.KeyVault(SecretUri=...)` — resolved by App Service | +| `ConnectionsMap__0__ServiceUrl` | `*` | +| `ConnectionsMap__0__Connection` | `ServiceConnection` | + +The `ClientSecret` setting uses an [App Service Key Vault reference](https://learn.microsoft.com/azure/app-service/app-reference-keyvault). App Service resolves the reference at startup using the web app managed identity. The actual secret value is never stored in App Service configuration. + +## Post-deploy validation + +```bash +# Liveness probe — should return 200 +curl https:///health/live + +# Readiness probe — should return 200 with {"status":"Healthy"} +curl https:///health/ready + +# Validate Bicep template syntax locally +az bicep build --file samples/dotnet/production-reference/infra/main.bicep + +# Run xUnit tests +dotnet test samples/dotnet/production-reference/tests/ProductionReference.Tests.csproj +``` + +Send a test message through the Bot Service channel (Teams or Agents Playground) to confirm end-to-end message delivery and state persistence. + +## Teardown + +```bash +az group delete --name "" --yes --no-wait +``` + +> **Note:** Deleting the resource group also deletes the Key Vault. If soft delete is enabled (as provisioned), the Key Vault is soft-deleted and retains its name reservation for `softDeleteRetentionInDays` (7 days as provisioned). To purge immediately: `az keyvault purge --name ""`. diff --git a/samples/dotnet/production-reference/README.md b/samples/dotnet/production-reference/README.md new file mode 100644 index 00000000..6a565e12 --- /dev/null +++ b/samples/dotnet/production-reference/README.md @@ -0,0 +1,196 @@ +# Production Reference — Support Triage Agent + +> **Sample tier:** Tier 3: Production reference +> **Language:** .NET +> **Scenario:** Stateful support triage agent — no LLM, durable conversation state, managed identity, health checks, telemetry, and infrastructure as code +> **Estimated setup time:** 30 minutes (local) · 60 minutes (Azure) +> **Supported channels:** Agents Playground, Web Chat, Teams, Microsoft 365 Copilot, custom app + +## What this sample demonstrates + +- A multi-turn support triage workflow that collects issue summary, impact, and contact preference without an LLM. +- Durable conversation-scoped state backed by Azure Blob Storage with two runtime paths: + - **Local development:** connection string to Azurite or a real storage account. + - **Cloud deployment:** managed identity (`DefaultAzureCredential`) via `AZURE_BLOB_STORAGE_CONTAINER_URI`. +- Azure Bot Service / Entra token validation in production via `TokenValidation__*` configuration. +- OpenTelemetry for ASP.NET Core, HttpClient, .NET runtime, and the Agents SDK meter and activity source, with OTLP and Azure Monitor export paths. +- Health endpoints (`/health/live`, `/health/ready`) used by App Service health check configuration. +- Infrastructure as code (`infra/main.bicep`) that provisions App Service, Blob Storage, Key Vault, Application Insights, Log Analytics, and all required role assignments. +- xUnit test suite covering dialog logic, storage configuration options, and health endpoint behavior. + +## What this sample does not demonstrate + +- LLM or AI inference — all dialog decisions are deterministic rule-based logic. +- Multi-region or geo-redundant storage configuration. +- State schema versioning or migration strategies. +- Slot-based blue/green deployments or advanced CI/CD pipelines. +- User-scoped state (only conversation-scoped state is used). +- Proactive messaging or outbound channel calls. + +## Architecture summary + +The agent runs as a Linux .NET 8 App Service with a system-assigned managed identity. Incoming messages from Azure Bot Service arrive at `POST /api/messages`, are validated by the Agents SDK token middleware, and are routed to `SupportTriageAgent`. The agent loads per-conversation state from Azure Blob Storage, advances the triage dialog by one step, persists the updated state, and returns a reply. + +**Runtime components:** + +| Component | Role | +|-----------|------| +| App Service (Linux .NET 8) | Hosts the agent; health check path set to `/health/live` | +| Azure Blob Storage (`agents-state` container) | Durable conversation state | +| Key Vault (`BotClientSecret`) | Bot client secret resolved at startup via App Service Key Vault reference | +| Application Insights + Log Analytics | Receives traces, metrics, and logs via Azure Monitor OpenTelemetry distro | +| Azure Bot Service (pre-existing) | Validates channel traffic; configured with `messagingEndpoint` output after deploy | + +**Managed identity role assignments provisioned by Bicep:** + +- `Storage Blob Data Contributor` scoped to the blob container — enables read/write of state blobs. +- `Key Vault Secrets User` scoped to the Key Vault — enables Key Vault reference resolution for `BotClientSecret`. + +## Production coverage checklist + +| Capability | Coverage in this sample | +|------------|------------------------| +| Identity and secrets | Entra token validation; system-assigned managed identity; bot client secret in Key Vault; no checked-in secrets | +| State and storage | Durable conversation state in Blob Storage; local (Azurite / connection string) and cloud (managed identity) paths | +| Observability | OpenTelemetry traces, metrics, and logs; OTLP and Azure Monitor export; Application Insights | +| Reliability | Health and readiness endpoints; fail-fast storage validation on startup; no in-memory fallback | +| Deployment | Bicep IaC; documented deployment sequence; role assignments automated | +| Testing | xUnit unit and integration tests for dialog, storage options, and health endpoints | +| Operations | This README, [DEPLOYMENT.md](DEPLOYMENT.md), and [RUNBOOK.md](RUNBOOK.md) | + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) for local storage emulation, **or** an Azure Storage account with a blob container +- An Azure Bot registration (required to connect a real channel; not required for local testing with [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview)) +- For Azure deployment: [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) with the Bicep extension, and an Azure subscription + +## Configure the sample + +### Local — Azurite emulator + +Start Azurite and set the development connection string: + +```bash +npx azurite --silent +``` + +```bash +# Bash +export AZURE_BLOB_STORAGE_CONNECTION_STRING="UseDevelopmentStorage=true" +``` + +```powershell +# PowerShell +$env:AZURE_BLOB_STORAGE_CONNECTION_STRING = "UseDevelopmentStorage=true" +``` + +### Local — real Azure Blob Storage + +```bash +export AZURE_BLOB_STORAGE_CONNECTION_STRING="" +``` + +Optionally override the container name (default is `agents-production-reference-state`): + +```bash +export AZURE_BLOB_STORAGE_CONTAINER_NAME="my-agent-state" +``` + +### Token validation (required when connecting a real channel) + +Edit `appsettings.json` or set environment variables: + +- Set `TokenValidation__Enabled` to `true`. +- Replace `{{ClientId}}` with your bot app ID and `{{TenantId}}` with your Entra tenant ID. +- Configure `Connections__ServiceConnection__Settings__AuthType`, `ClientId`, `ClientSecret`, and `AuthorityEndpoint` to match your bot registration. + +For local secrets, use [.NET user secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets): + +```bash +dotnet user-secrets set "TokenValidation__Enabled" "true" --project samples/dotnet/production-reference/ProductionReference.csproj +dotnet user-secrets set "Connections__ServiceConnection__Settings__ClientSecret" "" --project samples/dotnet/production-reference/ProductionReference.csproj +``` + +## Run locally + +```bash +cd samples/dotnet/production-reference +dotnet run +``` + +The agent starts on `http://localhost:3978` by default. + +## Verify the sample + +### Health endpoints + +```bash +curl http://localhost:3978/health/live +# Expected: 200 OK (liveness probe; no dependency checks) + +curl http://localhost:3978/health/ready +# Expected: 200 OK with {"status":"Healthy"} when storage is configured correctly +# Expected: 503 Service Unavailable when storage configuration is missing or invalid +``` + +### Message flow + +1. Open [Agents Playground](https://learn.microsoft.com/azure/bot-service/bot-service-overview) or Bot Framework Emulator and connect to `http://localhost:3978/api/messages`. +2. Send any message. +3. Confirm: the agent asks "Please describe your issue." +4. Send an issue description. +5. Confirm: the agent asks about impact level. +6. Send an impact level. +7. Confirm: the agent asks about contact preference. +8. Send a preference. +9. Confirm: the agent returns a summary of the collected information. + +### State persistence + +1. Complete one full triage conversation. +2. Stop and restart the agent process (`Ctrl+C`, then `dotnet run`). +3. In the same conversation (same conversation ID), send another message. +4. Confirm: the agent returns the final summary again, proving that state was restored from Blob Storage. + +## Azure deployment + +See [DEPLOYMENT.md](DEPLOYMENT.md) for the full deployment command sequence, parameter details, managed identity explanation, and post-deploy validation steps. + +## Run tests + +```bash +dotnet build samples/dotnet/production-reference/ProductionReference.csproj +dotnet test samples/dotnet/production-reference/tests/ProductionReference.Tests.csproj +``` + +Validate the Bicep template locally (generates `main.json` locally; keep that file untracked): + +```bash +az bicep build --file samples/dotnet/production-reference/infra/main.bicep +``` + +## Intentional shortcuts + +| Shortcut | Why it is acceptable in this sample | Production alternative | +|----------|-------------------------------------|------------------------| +| Single-region Blob Storage (`Standard_LRS`) | Sufficient for a reference sample; simplifies IaC | Use `Standard_GRS` or `Standard_RAGRS` for multi-region resiliency | +| `B1` App Service plan | Low-cost reference deployment | Right-size to `P1v3` or higher for production throughput | +| Manual bot registration | Bot Service provisioning varies significantly across organizations | Automate with a separate Bicep module or Azure CLI script | +| `softDeleteRetentionInDays: 7` on Key Vault | Minimum allowed; acceptable for a sample | Set to 30–90 days in production | +| Storage configuration validated on startup only | Keeps the health check fast and avoids live Azure calls in tests | Add a live blob-level probe for deeper readiness validation | + +## Known limits + +- Azure Blob Storage optimistic concurrency is not explicitly handled in `AgentStorageFactory`; high-concurrency scenarios on the same conversation may see contention. +- State blobs are not cleaned up after a conversation completes; configure a Blob Storage lifecycle management policy for retention in production. +- The triage dialog has no abandonment timeout; long-idle conversations retain their state indefinitely. + +## Next steps + +- **Simpler starting point:** [Persistent State](../persistent-state/README.md) — Tier 2 scenario starter that shows Blob Storage state without the full production infrastructure. +- **Operations:** [RUNBOOK.md](RUNBOOK.md) — health interpretation, telemetry queries, common incidents, and rollback guidance. +- **Deploy to Azure:** [DEPLOYMENT.md](DEPLOYMENT.md) — complete deployment sequence and post-deploy validation. +- **Authorize blobs with managed identity:** [Authorize access to blobs using managed identities](https://learn.microsoft.com/azure/storage/blobs/authorize-managed-identity) +- **OpenTelemetry .NET:** [Getting started with OpenTelemetry .NET](https://opentelemetry.io/docs/languages/net/) +- **Azure Monitor OpenTelemetry distro:** [Enable Azure Monitor OpenTelemetry for .NET](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=net) diff --git a/samples/dotnet/production-reference/RUNBOOK.md b/samples/dotnet/production-reference/RUNBOOK.md new file mode 100644 index 00000000..4fe38953 --- /dev/null +++ b/samples/dotnet/production-reference/RUNBOOK.md @@ -0,0 +1,227 @@ +# Runbook — Production Reference Sample + +This runbook covers health endpoint interpretation, telemetry queries, common incidents, rollback guidance, and known limits for the production-reference support triage agent. + +See [DEPLOYMENT.md](DEPLOYMENT.md) for initial setup and [README.md](README.md) for local development. + +## Health endpoints + +The agent exposes two health endpoints mapped in `Program.cs`: + +| Endpoint | Purpose | Healthy response | Unhealthy response | +|----------|---------|------------------|--------------------| +| `GET /health/live` | Liveness probe — confirms the process is running. No dependency checks. | `200 OK` (empty body) | Process crash / host unreachable | +| `GET /health/ready` | Readiness probe — confirms storage configuration is valid. | `200 OK` `{"status":"Healthy"}` | `503 Service Unavailable` `{"status":"Unhealthy"}` | + +**Interpretation:** + +- `/health/live` returning non-200 means the App Service process has crashed or the platform cannot route to the instance. App Service health check will restart the instance automatically. +- `/health/ready` returning 503 means `StorageConfigurationHealthCheck` detected an invalid or missing storage configuration. The agent will refuse to process messages. Check the `AZURE_BLOB_STORAGE_CONTAINER_URI` app setting and the managed identity role assignment. + +**App Service health check configuration:** The Bicep template sets `healthCheckPath: /health/live`. This path is used by App Service to auto-restart unhealthy instances. The readiness endpoint is available for external monitoring or deployment pipelines but is not wired to App Service auto-restart by default. + +## Telemetry + +### Location + +All telemetry is sent to Application Insights, which is backed by a Log Analytics workspace. Both resources are provisioned by the Bicep template. + +Navigate to your Application Insights resource in the [Azure Portal](https://portal.azure.com) and open **Logs** to run Kusto queries. + +### Example Kusto queries + +**Recent requests (last 30 minutes):** + +```kusto +requests +| where timestamp > ago(30m) +| project timestamp, name, url, resultCode, duration, success +| order by timestamp desc +| take 50 +``` + +**Failed requests:** + +```kusto +requests +| where timestamp > ago(1h) and success == false +| project timestamp, name, url, resultCode, duration, customDimensions +| order by timestamp desc +``` + +**Dependency failures (outbound calls to Azure Storage or Bot Service):** + +```kusto +dependencies +| where timestamp > ago(1h) and success == false +| project timestamp, type, target, name, resultCode, duration, data +| order by timestamp desc +``` + +**Exceptions:** + +```kusto +exceptions +| where timestamp > ago(1h) +| project timestamp, type, outerMessage, method, assembly, customDimensions +| order by timestamp desc +| take 50 +``` + +**Requests with high latency (over 2 seconds):** + +```kusto +requests +| where timestamp > ago(1h) and duration > 2000 +| project timestamp, name, url, duration, resultCode +| order by duration desc +``` + +**Health check probe results:** + +```kusto +requests +| where timestamp > ago(1h) and url contains "/health/" +| summarize count(), avg(duration), countif(success == false) by url, bin(timestamp, 5m) +| order by timestamp desc +``` + +**Custom metrics from the Agents SDK meter:** + +```kusto +customMetrics +| where timestamp > ago(1h) and name startswith "Microsoft.Agents" +| project timestamp, name, value, valueCount, valueSum +| order by timestamp desc +``` + +## Common incidents + +### Incident: Storage Blob Data Contributor role missing + +**Symptom:** `/health/ready` returns 503. Application logs show `AuthorizationPermissionMismatch` or `403 Forbidden` when the agent attempts to read or write a state blob. + +**Cause:** The managed identity does not have the `Storage Blob Data Contributor` role on the blob container, or the role assignment has not propagated yet. + +**Remediation:** + +1. Confirm the web app has a system-assigned managed identity: + ```bash + az webapp identity show --resource-group "" --name "" + ``` +2. Verify the role assignment: + ```bash + az role assignment list \ + --assignee "" \ + --scope "" \ + --query "[].{role:roleDefinitionName, scope:scope}" + ``` +3. If missing, add the role assignment: + ```bash + az role assignment create \ + --assignee "" \ + --role "Storage Blob Data Contributor" \ + --scope "" + ``` +4. Wait 30–60 seconds for propagation, then restart the web app: + ```bash + az webapp restart --resource-group "" --name "" + ``` + +### Incident: Key Vault reference failure + +**Symptom:** App Service configuration shows the `ClientSecret` setting value as `Microsoft.KeyVault reference is not resolvable`. Agent startup fails with authentication errors. + +**Cause:** The managed identity does not have the `Key Vault Secrets User` role on the Key Vault, or the Key Vault name or secret URI has changed. + +**Remediation:** + +1. Verify the Key Vault Secrets User role assignment: + ```bash + az role assignment list \ + --assignee "" \ + --scope "/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/" \ + --query "[].{role:roleDefinitionName}" + ``` +2. If missing, add it: + ```bash + az role assignment create \ + --assignee "" \ + --role "Key Vault Secrets User" \ + --scope "/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/" + ``` +3. Confirm the `BotClientSecret` secret exists in the Key Vault: + ```bash + az keyvault secret show --vault-name "" --name "BotClientSecret" + ``` +4. In the App Service **Configuration** blade, verify that `Connections__ServiceConnection__Settings__ClientSecret` shows a valid Key Vault reference URI matching the secret version. +5. Restart the web app. + +### Incident: Token validation mismatch + +**Symptom:** The agent returns `401 Unauthorized` for all incoming messages. Application Insights shows failed requests with `401` result code. + +**Cause:** `TokenValidation__Audiences__0` or `TokenValidation__TenantId` does not match the bot registration, or `TokenValidation__Enabled` is `false` while the bot is connecting over a real channel. + +**Remediation:** + +1. Confirm the Bot Service app ID matches the `botClientId` deployment parameter: + ```bash + az bot show --resource-group "" --name "" --query "properties.msaAppId" + ``` +2. Verify App Service settings: + ```bash + az webapp config appsettings list --resource-group "" --name "" \ + --query "[?name=='TokenValidation__Enabled' || name=='TokenValidation__Audiences__0' || name=='TokenValidation__TenantId']" + ``` +3. Correct any mismatch and restart the web app. + +### Incident: Readiness probe unhealthy after deploy + +**Symptom:** `/health/ready` returns 503 immediately after a new deployment. + +**Cause:** `Program.cs` calls `storageOptions.Validate()` at startup. If `AZURE_BLOB_STORAGE_CONTAINER_URI` is not set (for example, if the Bicep deploy did not complete before the app package was deployed), startup validation fails. + +**Remediation:** + +1. Check that the Bicep deployment completed successfully and the `storageContainerUri` output is present. +2. Verify the `AZURE_BLOB_STORAGE_CONTAINER_URI` app setting is set: + ```bash + az webapp config appsettings list --resource-group "" --name "" \ + --query "[?name=='AZURE_BLOB_STORAGE_CONTAINER_URI']" + ``` +3. If missing, redeploy the Bicep template or set it manually and restart. + +## Rollback guidance + +### Rollback an application package + +App Service maintains a deployment history. To swap back to the previous package: + +1. List recent deployments: + ```bash + az webapp deployment list-publishing-credentials --resource-group "" --name "" + ``` +2. If using deployment slots, swap back: + ```bash + az webapp deployment slot swap \ + --resource-group "" \ + --name "" \ + --slot staging \ + --target-slot production + ``` +3. Without slots, redeploy the previous `deploy.zip` package via `az webapp deploy`. + +### Rollback infrastructure changes + +Bicep deployments are incremental by default. To revert a Bicep change, redeploy the previous version of `main.bicep` with the same parameters. Role assignments are idempotent; re-running will not duplicate them. + +> **Do not delete and recreate the Key Vault** unless necessary. Soft delete means the vault name is reserved for `softDeleteRetentionInDays`. Purge first if you need to reuse the name. + +## Known limits + +- **Storage concurrency:** Azure Blob Storage uses optimistic concurrency via ETags. The Agents SDK performs read-modify-write on state blobs. Concurrent requests on the same conversation may result in a concurrency conflict; the SDK retries, but very high concurrency on a single conversation is not a tested scenario. +- **State retention:** State blobs are never automatically deleted. Configure a [Blob Storage lifecycle management policy](https://learn.microsoft.com/azure/storage/blobs/lifecycle-management-overview) to expire old conversation state. +- **Health check depth:** The readiness check validates configuration shape only; it does not perform a live read/write against Blob Storage. A misconfigured managed identity will pass the readiness check but fail at runtime. +- **Bot registration:** The bot registration is not managed by Bicep. If the bot app ID or tenant changes, app settings must be updated manually and the app restarted. +- **Scale-out:** The `B1` plan does not support autoscale. Upgrade the plan SKU and configure autoscale rules before using this sample as a production baseline. From f1446060a2d69ba1964b9cd6538df2df70c23773 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:39:57 -0400 Subject: [PATCH 30/33] Correct production reference operations docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/production-reference/DEPLOYMENT.md | 4 ++-- samples/dotnet/production-reference/README.md | 6 +++--- samples/dotnet/production-reference/RUNBOOK.md | 15 +++++++++------ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/samples/dotnet/production-reference/DEPLOYMENT.md b/samples/dotnet/production-reference/DEPLOYMENT.md index bddceb18..35698ae2 100644 --- a/samples/dotnet/production-reference/DEPLOYMENT.md +++ b/samples/dotnet/production-reference/DEPLOYMENT.md @@ -170,10 +170,10 @@ The `ClientSecret` setting uses an [App Service Key Vault reference](https://lea ## Post-deploy validation ```bash -# Liveness probe — should return 200 +# Liveness probe — should return 200, body: Healthy (plain text) curl https:///health/live -# Readiness probe — should return 200 with {"status":"Healthy"} +# Readiness probe — should return 200, body: Healthy (plain text) curl https:///health/ready # Validate Bicep template syntax locally diff --git a/samples/dotnet/production-reference/README.md b/samples/dotnet/production-reference/README.md index 6a565e12..3602d38a 100644 --- a/samples/dotnet/production-reference/README.md +++ b/samples/dotnet/production-reference/README.md @@ -127,11 +127,11 @@ The agent starts on `http://localhost:3978` by default. ```bash curl http://localhost:3978/health/live -# Expected: 200 OK (liveness probe; no dependency checks) +# Expected: 200 OK, body: Healthy (plain text liveness probe; no dependency checks) curl http://localhost:3978/health/ready -# Expected: 200 OK with {"status":"Healthy"} when storage is configured correctly -# Expected: 503 Service Unavailable when storage configuration is missing or invalid +# Expected: 200 OK, body: Healthy (plain text) when storage is configured correctly +# Expected: 503 Service Unavailable, body: Unhealthy (plain text) when storage configuration is missing or invalid ``` ### Message flow diff --git a/samples/dotnet/production-reference/RUNBOOK.md b/samples/dotnet/production-reference/RUNBOOK.md index 4fe38953..4a2a9381 100644 --- a/samples/dotnet/production-reference/RUNBOOK.md +++ b/samples/dotnet/production-reference/RUNBOOK.md @@ -10,8 +10,8 @@ The agent exposes two health endpoints mapped in `Program.cs`: | Endpoint | Purpose | Healthy response | Unhealthy response | |----------|---------|------------------|--------------------| -| `GET /health/live` | Liveness probe — confirms the process is running. No dependency checks. | `200 OK` (empty body) | Process crash / host unreachable | -| `GET /health/ready` | Readiness probe — confirms storage configuration is valid. | `200 OK` `{"status":"Healthy"}` | `503 Service Unavailable` `{"status":"Unhealthy"}` | +| `GET /health/live` | Liveness probe — confirms the process is running. No dependency checks. | `200 OK` body: `Healthy` (plain text) | Process crash / host unreachable | +| `GET /health/ready` | Readiness probe — confirms storage configuration is valid. | `200 OK` body: `Healthy` (plain text) | `503 Service Unavailable` body: `Unhealthy` (plain text) | **Interpretation:** @@ -198,10 +198,13 @@ customMetrics App Service maintains a deployment history. To swap back to the previous package: -1. List recent deployments: - ```bash - az webapp deployment list-publishing-credentials --resource-group "" --name "" - ``` +1. Inspect deployment history without exposing credentials — use one of: + - **Azure portal:** App Service → Deployment Center → Deployment Logs shows timestamps, statuses, and commit IDs. + - **Kudu SCM console:** `https://.scm.azurewebsites.net/` → Deployments (no credentials printed to the console). + - **Azure CLI (safe):** + ```bash + az webapp log deployment list --resource-group "" --name "" + ``` 2. If using deployment slots, swap back: ```bash az webapp deployment slot swap \ From e2b7961990425845c0576daa8442dea55356f0b5 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:09:05 -0400 Subject: [PATCH 31/33] Address production reference final review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Telemetry/TelemetryExtensions.cs | 36 +++++++--- .../tests/HealthEndpointTests.cs | 12 ++++ .../tests/TelemetryExtensionsTests.cs | 66 +++++++++++++++++++ 3 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs diff --git a/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs b/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs index af5defb5..ebedcc94 100644 --- a/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs +++ b/samples/dotnet/production-reference/Telemetry/TelemetryExtensions.cs @@ -25,35 +25,50 @@ public static WebApplicationBuilder ConfigureProductionReferenceTelemetry( { string? otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; string? appInsightsConnStr = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + bool useOtlp = !string.IsNullOrEmpty(otlpEndpoint); + bool useAzureMonitor = !string.IsNullOrEmpty(appInsightsConnStr); var otel = builder.Services.AddOpenTelemetry() .ConfigureResource(resource => resource .AddService("ProductionReference")) .WithTracing(tracing => { - tracing - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddSource(AgentsTelemetry.SourceName); + tracing.AddSource(AgentsTelemetry.SourceName); - if (!string.IsNullOrEmpty(otlpEndpoint)) + // Azure Monitor distro supplies ASP.NET Core and HttpClient instrumentation. + // Register those manually only for the OTLP-only path to avoid duplicate spans. + if (!useAzureMonitor) + { + tracing + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + } + + if (useOtlp) tracing.AddOtlpExporter(); }) .WithMetrics(metrics => { metrics - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddMeter(AgentsTelemetry.SourceName); - if (!string.IsNullOrEmpty(otlpEndpoint)) + // Azure Monitor distro supplies ASP.NET Core and HttpClient instrumentation. + // Register those manually only for the OTLP-only path to avoid duplicate metric series. + if (!useAzureMonitor) + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + } + + if (useOtlp) metrics.AddOtlpExporter(); }) .WithLogging( logging => { - if (!string.IsNullOrEmpty(otlpEndpoint)) + if (useOtlp) logging.AddOtlpExporter(); }, options => @@ -62,9 +77,10 @@ public static WebApplicationBuilder ConfigureProductionReferenceTelemetry( options.IncludeScopes = true; }); - if (!string.IsNullOrEmpty(appInsightsConnStr)) + if (useAzureMonitor) otel.UseAzureMonitor(); return builder; } } + diff --git a/samples/dotnet/production-reference/tests/HealthEndpointTests.cs b/samples/dotnet/production-reference/tests/HealthEndpointTests.cs index 0ef88cca..9d5a6b02 100644 --- a/samples/dotnet/production-reference/tests/HealthEndpointTests.cs +++ b/samples/dotnet/production-reference/tests/HealthEndpointTests.cs @@ -66,6 +66,17 @@ public async Task LivenessEndpoint_Returns200_WithoutAzureDependencies() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + [Fact] + public async Task ReadinessEndpoint_Returns200_WhenStorageConfigurationIsValid() + { + var client = _factory.CreateClient(); + var response = await client.GetAsync("/health/ready"); + string body = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Healthy", body); + } + [Fact] public async Task StorageConfigurationHealthCheck_Unhealthy_WhenNoStorageConfigured() { @@ -117,3 +128,4 @@ public async Task StorageConfigurationHealthCheck_Healthy_WhenContainerUriConfig Assert.Equal(HealthStatus.Healthy, result.Status); } } + diff --git a/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs b/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs new file mode 100644 index 00000000..4b9a4d6d --- /dev/null +++ b/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Azure.Monitor.OpenTelemetry.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace ProductionReference.Tests; + +public class TelemetryExtensionsTests +{ + [Fact] + public void ConfigureProductionReferenceTelemetry_registers_http_instrumentation_for_otlp_only() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty()); + + builder.ConfigureProductionReferenceTelemetry(); + + Assert.Equal(1, CountHttpClientInstrumentationOptions(builder.Services)); + } + + [Fact] + public void ConfigureProductionReferenceTelemetry_does_not_duplicate_http_instrumentation_when_azure_monitor_is_enabled() + { + WebApplicationBuilder baseline = CreateAzureMonitorBuilder(); + baseline.Services.AddOpenTelemetry().UseAzureMonitor(); + + WebApplicationBuilder builder = CreateAzureMonitorBuilder(); + builder.ConfigureProductionReferenceTelemetry(); + + Assert.Equal( + CountOpenTelemetryBuilderCallbacks(baseline.Services, "OpenTelemetry.Trace.IConfigureTracerProviderBuilder") + 4, + CountOpenTelemetryBuilderCallbacks(builder.Services, "OpenTelemetry.Trace.IConfigureTracerProviderBuilder")); + Assert.Equal( + CountOpenTelemetryBuilderCallbacks(baseline.Services, "OpenTelemetry.Metrics.IConfigureMeterProviderBuilder") + 6, + CountOpenTelemetryBuilderCallbacks(builder.Services, "OpenTelemetry.Metrics.IConfigureMeterProviderBuilder")); + } + + private static WebApplicationBuilder CreateAzureMonitorBuilder() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty()); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["APPLICATIONINSIGHTS_CONNECTION_STRING"] = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://example.com/", + }); + return builder; + } + + private static int CountHttpClientInstrumentationOptions(IServiceCollection services) + { + return services.Count(service => + service.ServiceType.FullName?.Contains("HttpClientTraceInstrumentationOptions", StringComparison.Ordinal) == true); + } + + private static int CountOpenTelemetryBuilderCallbacks(IServiceCollection services, string serviceTypeName) + { + return services.Count(service => + string.Equals(service.ServiceType.FullName, serviceTypeName, StringComparison.Ordinal)); + } +} + From 6f62b46ed6a7ca3afb1f6d5efbab7c158564f067 Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:12:31 -0400 Subject: [PATCH 32/33] Stabilize production reference telemetry tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/TelemetryExtensionsTests.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs b/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs index 4b9a4d6d..aae41825 100644 --- a/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs +++ b/samples/dotnet/production-reference/tests/TelemetryExtensionsTests.cs @@ -17,7 +17,7 @@ public class TelemetryExtensionsTests [Fact] public void ConfigureProductionReferenceTelemetry_registers_http_instrumentation_for_otlp_only() { - WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty()); + WebApplicationBuilder builder = CreateBuilderWithConfiguration(null, null); builder.ConfigureProductionReferenceTelemetry(); @@ -33,6 +33,8 @@ public void ConfigureProductionReferenceTelemetry_does_not_duplicate_http_instru WebApplicationBuilder builder = CreateAzureMonitorBuilder(); builder.ConfigureProductionReferenceTelemetry(); + // The +4/+6 deltas come from this extension adding the Agents SDK source/meter, + // runtime metrics, resource configuration, and provider callbacks on top of the distro. Assert.Equal( CountOpenTelemetryBuilderCallbacks(baseline.Services, "OpenTelemetry.Trace.IConfigureTracerProviderBuilder") + 4, CountOpenTelemetryBuilderCallbacks(builder.Services, "OpenTelemetry.Trace.IConfigureTracerProviderBuilder")); @@ -42,11 +44,21 @@ public void ConfigureProductionReferenceTelemetry_does_not_duplicate_http_instru } private static WebApplicationBuilder CreateAzureMonitorBuilder() + { + return CreateBuilderWithConfiguration( + "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://example.com/", + null); + } + + private static WebApplicationBuilder CreateBuilderWithConfiguration( + string? appInsightsConnectionString, + string? otlpEndpoint) { WebApplicationBuilder builder = WebApplication.CreateBuilder(Array.Empty()); builder.Configuration.AddInMemoryCollection(new Dictionary { - ["APPLICATIONINSIGHTS_CONNECTION_STRING"] = "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://example.com/", + ["APPLICATIONINSIGHTS_CONNECTION_STRING"] = appInsightsConnectionString, + ["OTEL_EXPORTER_OTLP_ENDPOINT"] = otlpEndpoint, }); return builder; } From bb88468fdbd010efc11f81bfea4e78ed669a668b Mon Sep 17 00:00:00 2001 From: Andy Matkin <71270386+andrewmatkin@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:45:22 -0400 Subject: [PATCH 33/33] Fix sample catalog links after main merge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/dotnet/README.md | 3 ++- samples/nodejs/README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/dotnet/README.md b/samples/dotnet/README.md index 0d04c02a..0fd8233f 100644 --- a/samples/dotnet/README.md +++ b/samples/dotnet/README.md @@ -11,7 +11,7 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Streaming Agent |Tier 2: Scenario starter|Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)| |Copilot Studio Client|Tier 2: Scenario starter|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| |Copilot Studio Skill |Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| -|RetrievalBot Sample with Semantic Kernel|Tier 2: Scenario starter|A simple Retrieval Agent that is hosted on an Asp.net core web service. |[RetrievalBot](RetrievalBot/README.md)| +|RetrievalBot Sample with Semantic Kernel|Tier 2: Scenario starter|A simple Retrieval Agent that is hosted on an Asp.net core web service. |[RetrievalBot](retrieval-agent/README.md)| |MultiAgent|Tier 2: Scenario starter|Demonstrates multiple AgentApplication in the same host|[MultiAgent](multiagent/README.md)| |GenesysHandoff|Tier 2: Scenario starter|Demonstrates how a Microsoft Copilot Studio Agent (bot) can seamlessly **hand off a conversation to a live agent** in **Genesys Cloud**.|[GenesysHandoff](genesys-handoff/README.md)| |Proactive|Tier 2: Scenario starter|Demonstrates the basics of a proactive conversation using in-code and Http triggers.|[Proactive](proactive/README.md)| @@ -21,3 +21,4 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |Named Pipe Agent|Tier 2: Scenario starter|Pipe-only echo agent for the DirectLine App Service extension (DirectLineFlex)|[named-pipe-agent](named-pipe-agent/README.md)| |Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| |Production Reference|Tier 3: Production reference|Deployable support triage agent with managed identity, durable state, health checks, telemetry, Bicep, tests, and runbook|[production-reference](production-reference/README.md)| + diff --git a/samples/nodejs/README.md b/samples/nodejs/README.md index c438c791..f9427cd6 100644 --- a/samples/nodejs/README.md +++ b/samples/nodejs/README.md @@ -8,7 +8,7 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |AutoSignIn|Tier 2: Scenario starter|Simple OAuth agent using Graph|[auto-signin](auto-signin/README.md)| |OBOAuthorization|Tier 2: Scenario starter|OBO to Copilot Studio Agent |[obo-authorization](obo-authorization/README.md)| |Cards|Tier 2: Scenario starter|Sending and receiving adaptive cards|[cards](cards/README.md)| -|Cards Streaming|Tier 2: Scenario starter|Streaming adaptive cards|[cards-streaming](cards-streaming/README.md)| +|Cards Streaming|Tier 2: Scenario starter|Streaming adaptive cards|[cards-streaming](cards/README.md)| |Streaming Agent |Tier 2: Scenario starter|Streams OpenAI responses|[azure-ai-streaming](azure-ai-streaming/README.md)| |Copilot Studio Client|Tier 2: Scenario starter|Console app to consume a Copilot Studio Agent|[copilotstudio-client](copilotstudio-client/README.md)| |Copilot Studio Skill|Tier 2: Scenario starter|Call the echo bot from a Copilot Studio skill |[copilotstudio-skill](copilotstudio-skill/README.md)| @@ -16,3 +16,4 @@ These samples are Tier 1 or Tier 2 starter samples unless a README explicitly la |OTel Agent|Tier 2: Scenario starter|Agent with OTel instrumentation |[otel](otel/README.md)| |Named Pipe Agent|Tier 2: Scenario starter|Pipe-only echo agent for the DirectLine App Service extension (DirectLineFlex)|[named-pipe-agent](named-pipe-agent/README.md)| |Persistent State|Tier 2: Scenario starter|Durable conversation state backed by Azure Blob Storage|[persistent-state](persistent-state/README.md)| +