Skip to content

Repository files navigation

Agent Publish Sample

This repository is a minimal .NET 10 reference implementation for an independent software vendor that already owns an agent orchestration layer and wants customers to use it from Microsoft 365 Copilot or Microsoft Teams.

The application is a custom engine agent. Microsoft 365 supplies the user experience and authenticated messaging channel. The ISV retains responsibility for the model, instructions, tools, workflow, hosting, tenant admission, support and commercial publication.

The sample uses:

  • ASP.NET Core and a small Blazor Interactive Server diagnostic page;
  • .NET Aspire for local composition, health checks and the developer dashboard;
  • Microsoft 365 Agents SDK for the channel endpoint;
  • Microsoft Agent Framework for portable orchestration;
  • an existing Microsoft Foundry project and model deployment supplied by the operator;
  • Microsoft Learn MCP as a public, read-only demonstration tool;
  • Azure Container Apps as the only deployment example;
  • OpenTelemetry with Aspire locally and Application Insights in Azure;
  • Microsoft 365 Agents Toolkit CLI for manifest validation and packaging.

No Azure resource is deployed merely by building or validating this repository.

Architectural result

flowchart LR
    U[Customer user] --> M[Microsoft 365 Copilot or Teams]
    M --> B[Azure Bot Service]
    subgraph G["Gateway Container App<br/>AgentPublishSample.Web"]
        A[Microsoft 365 Agents SDK adapter<br/>POST /api/messages] --> T[AgentGateway service<br/>tenant admission, validation<br/>and normalisation]
        W[Blazor diagnostic page<br/>development only] --> T
    end
    B --> A
    T --> C[Portable Microsoft Agent Framework core]
    C --> F[Existing Foundry project<br/>operator-selected model deployment]
    C --> L[Microsoft Learn MCP]
    A --> O[OpenTelemetry]
    T --> O
    C --> O
    O --> D[Aspire Dashboard locally]
    O --> I[Application Insights in Azure]
Loading

What gateway means

gateway is the deployment and Aspire resource name for the complete AgentPublishSample.Web process. It is not another agent or a separate orchestration service. In Azure, that process runs in the gateway Container App. Locally, Aspire starts the same process as a resource named gateway.

Inside the process, M365Agent adapts authenticated Microsoft 365 activities. AgentGateway is an internal application service that validates input, applies tenant admission, establishes safe correlation context and invokes the portable core. The development-only Blazor page calls the same AgentGateway, which allows the core path to be demonstrated without pretending that the page is a Microsoft 365 channel.

The gateway is assembled from two .NET projects:

Project Role in the gateway
AgentPublishSample.Web Executable host. Authenticate the channel, derive trusted conversation context, admit the customer tenant, normalise a text activity, preserve correlation and translate the response
AgentPublishSample.Agent Referenced portable core. Own the prompt, model choice, Microsoft Agent Framework orchestration, MCP tools and protocol-neutral request and response contracts

AgentPublishSample.Web references AgentPublishSample.Agent, so both assemblies run in one process and one Container App. A production product may deploy them independently when scaling, security isolation, team ownership or release cadence requires separate services. That change must preserve the same trust boundary.

Identity model

Production uses one user-assigned managed identity:

  1. Bicep creates the identity and attaches it to the Container App.
  2. Azure Bot Service registers its client ID with msaAppType set to UserAssignedMSI.
  3. Microsoft 365 Agents SDK uses AuthType=UserManagedIdentity.
  4. The same identity receives least-privilege access to the existing Foundry account and pulls the image from Azure Container Registry.
  5. The identity client ID becomes BOT_ID in the Microsoft 365 app manifest.

There is no production Bot client secret and no Key Vault resource in this sample. The workload identity belongs to the ISV tenant. Store distribution allows the messaging app to be installed by customers, but does not grant Microsoft Graph access or consent to customer data.

A developer workstation cannot use the Container App identity. m365agents.local.yml therefore creates a separate, single-tenant AzureADMyOrg development application and writes its generated secret to the ignored .env.local.user file. This is a local preview exception, not a production pattern.

Trust and tenant controls

The following controls are independent:

  1. Channel authentication proves that the request was delivered through the configured Microsoft 365 and Bot Framework channel.
  2. Conversation tenant extraction reads the tenant from authenticated activity context. A header, query parameter or message body cannot establish tenant identity.
  3. ISV admission checks the trusted tenant against an onboarding allow-list. Authentication alone does not entitle a customer to use the service.
  4. Downstream authorisation must be applied by every customer system or tool. Admission to the agent does not grant access to payroll, HR, finance or Microsoft Graph data.

General telemetry records only a 12-character SHA-256-derived tenant correlation value. Prompt, response, token and raw tenant content are excluded by default.

See Architecture and Security and multitenancy.

Environment inputs

The repository contains no environment-specific Azure subscription, tenant, publisher or resource identity. Before running or deploying it, provide:

Setting Required value
Azure subscription <azure-subscription-id>
Azure region <azure-region>
Foundry project endpoint https://<foundry-resource>.services.ai.azure.com/api/projects/<foundry-project>
Model deployment <model-deployment-name>
Microsoft Learn MCP https://learn.microsoft.com/api/mcp
Microsoft 365 manifest Schema 1.21

The angle-bracketed values are deliberate non-runnable placeholders. The infrastructure template does not create or modify the Foundry account, project or model deployment.

GUID-shaped values that remain in source have a defined public purpose. Repeated values such as 11111111-... are synthetic test data. Bicep role definition GUIDs identify documented Azure built-in roles. Issuer GUIDs in AspNetExtensions.cs are Microsoft-owned platform trust constants retained from the attributed upstream Microsoft 365 Agents SDK helper; they are not customer or deployment identities.

Repository map

Path Purpose
src/AgentPublishSample.Agent/ Portable Microsoft Agent Framework core
src/AgentPublishSample.Web/ Microsoft 365 adapter and Blazor diagnostic page
src/AgentPublishSample.AppHost/ One-resource Aspire local topology
src/AgentPublishSample.ServiceDefaults/ OpenTelemetry, resilience and health defaults
tests/ Core, adapter and project-boundary tests
infra/ ACA, identity, Bot Service, monitoring, ACR and Foundry RBAC Bicep
azure.yaml One gateway service for Azure Developer CLI
appPackage/ Templated Microsoft 365 manifest and icons used to build the upload ZIP
m365agents.local.yml Local single-tenant Bot and app registration workflow
m365agents.yml Shared-environment Microsoft 365 registration, packaging and organisational publication workflow
env/ Non-secret environment examples
scripts/Test-AppPackage.ps1 Strict source, manifest, icon and deterministic ZIP checks
scripts/Validate-AppPackage.ps1 Repository checks followed by Agents Toolkit validation
scripts/gateway.Dockerfile Linux AMD64 gateway image

Generated packages belong under appPackage/build/ and must not be committed.

Microsoft 365 publication assets

An ISV releases two related artefacts:

  1. the gateway image, which contains the running .NET application and is deployed to Azure Container Apps by azd; and
  2. the Microsoft 365 app package, which contains only the manifest and icons and is distributed through Microsoft 365 administration or Partner Center.

The package does not contain the agent code, model, MCP server, Bicep, credentials or Container App endpoint implementation. It tells Microsoft 365 how the externally hosted agent is identified, presented and acquired.

flowchart TD
    S[.NET source and Bicep] --> AZD[azd provision and deploy]
    AZD --> ACA[Gateway on Azure Container Apps]
    AZD -->|BOT_ID and GATEWAY_DOMAIN| E[env/.env.dev]

    P[appPackage source<br/>manifest.json and two icons] --> Y[m365agents.yml<br/>Agents Toolkit lifecycle]
    E --> Y
    Y --> Z[Rendered Microsoft 365 app package ZIP]

    Z --> T[Personal sideload<br/>controlled testing]
    Z --> O[Organisation catalogue submission<br/>admin approval]
    Z --> C[Partner Center submission<br/>certification and release]

    T --> I[Installed custom engine agent]
    O --> I
    C --> I
    I -->|BOT_ID| B[Azure Bot Service]
    B --> ACA
Loading

m365agents.yml is the publication workflow

m365agents.yml is interpreted by Microsoft 365 Agents Toolkit. It is not ASP.NET Core configuration and it is not an Azure deployment template. Its lifecycle names have the following meaning in this repository:

Lifecycle Command What it does here What it does not do
provision atk provision --env dev Creates the Microsoft 365 application record, writes TEAMS_APP_ID, and validates the source manifest It does not create ACA, Azure Bot, managed identity or the model
deploy atk deploy --env dev Renders placeholders, creates and validates the ZIP, and updates the Microsoft 365 application record It does not deploy the gateway image
publish atk publish --env dev Rebuilds the package and submits it to the organisation catalogue approval workflow It does not approve the app or create a Partner Center Marketplace offer

The environment name dev is an operator-defined label. An ISV should normally use separate Toolkit environments for development, certification and production, with separate generated identifiers where its release policy requires them.

m365agents.local.yml has a different purpose. It creates a development-only, single-tenant Entra application, service principal, client secret and Bot registration for local preview through a secure tunnel. Those local credentials must never be copied into the Azure or Marketplace package workflow.

appPackage/ is the source of the installable contract

The source folder intentionally contains only:

appPackage/
|-- manifest.json
|-- colour.png
`-- outline.png

manifest.json is strict JSON, so it cannot contain comments. A README is also kept outside this folder because this sample deliberately rejects unexpected package source files. The explanation therefore lives here and in Microsoft 365 publication.

The principal manifest sections are:

Manifest field Meaning for this custom engine agent
id Resolves from TEAMS_APP_ID. This identifies the Microsoft 365 app package and catalogue record
developer, name, description Publisher and user-facing metadata that must agree with the Partner Center listing
icons Names the 192 x 192 colour icon and 32 x 32 transparent outline icon included in the ZIP
bots[0].botId Resolves from BOT_ID. This identifies the Azure Bot and production managed identity
bots[0].scopes Uses personal because the minimum custom engine experience is a personal agent
copilotAgents.customEngineAgents[0] Declares that the same personal Bot is a custom engine agent available to Microsoft 365 Copilot
permissions Empty in this sample. Installation does not grant Microsoft Graph or business-system access
validDomains Contains the gateway DNS host only. Azure Bot separately owns the full /api/messages routing endpoint
defaultInstallScope Requests personal installation

The Bot declaration and custom engine declaration deliberately use the same BOT_ID. Microsoft 365 installs the app described by TEAMS_APP_ID, then routes agent activities through the Azure Bot identified by BOT_ID.

Environment placeholders become a release package

The checked-in manifest is a template and must not be uploaded directly. Agents Toolkit resolves ${{NAME}} expressions from the selected ignored environment:

Placeholder Source Release significance
TEAMS_APP_ID Created by teamsApp/create Stable identity of the Microsoft 365 app
BOT_ID azd output from the gateway managed identity Must match Azure Bot, the Agents SDK connection, bots[0].botId, and the custom engine agent ID
GATEWAY_DOMAIN ACA ingress output Public DNS host admitted by the app manifest
Names and publisher fields ISV release metadata Must match the approved product and Partner Center submission
Privacy, terms and support URLs ISV public web properties Must be public HTTPS pages and operational before certification

teamsApp/zipAppPackage writes the rendered ZIP under appPackage/build/. The uploadable ZIP has exactly three root entries:

manifest.json
colour.png
outline.png

The generated ZIP is the technical artefact used for personal testing, organisation catalogue submission and Partner Center certification. atk validate checks technical structure. It does not establish legal ownership, privacy accuracy, operational readiness or Marketplace certification.

Prerequisites

  • .NET 10 SDK
  • Azure CLI
  • Azure Developer CLI
  • Microsoft 365 Agents Toolkit CLI
  • An Azure identity that can read the existing Foundry project
  • For Azure preparation, permission to validate deployments and inspect providers
  • For provisioning, permission to create the documented Azure resources and role assignments
  • For Microsoft 365 preview, an eligible Microsoft 365 development tenant
  • For public release, an enrolled and verified Partner Center publisher

Visual Studio supports .NET development and the Microsoft 365 Agents Toolkit workload. Visual Studio Code supports the Agents Toolkit extension. This repository uses the CLI as the canonical workflow because its commands can be repeated in a terminal and CI pipeline.

Tested package compatibility boundary

Directory.Packages.props centrally controls direct dependency versions. The following set is the tested runtime boundary for this sample:

Package Version Status and reason
Microsoft.Agents.AI 1.16.0 Stable Microsoft Agent Framework core
Microsoft.Agents.AI.Foundry 1.16.0-preview.260730.1 Preview adapter needed to create the Foundry-backed chat client
Azure.AI.Projects 2.1.0-beta.4 Preview Foundry projects client required by the adapter
Microsoft.Extensions.AI 10.8.3 Shared AI abstractions used by the portable core
Microsoft.Extensions.AI.OpenAI 10.6.0 Transitive version selected by the Foundry adapter
OpenAI 2.10.0 Transitive version compatible with Azure.AI.Projects 2.1.0-beta.4
ModelContextProtocol 2.0.0 Microsoft Learn MCP client
Microsoft 365 Agents SDK packages 1.7.129 Channel hosting and authentication
Aspire.Hosting.AppHost 13.4.6 Local application composition
OpenTelemetry packages 1.17.0 Traces, metrics and logs

The Foundry adapter and Azure projects client remain preview dependencies even though the sample deploys its process to Azure Container Apps. They are used because the portable core calls an existing model deployment through the Foundry project endpoint. Preview packages require explicit review before a production release.

Do not add a direct reference to Microsoft.Extensions.AI.OpenAI or OpenAI merely to select a newer version. Microsoft.Agents.AI.Foundry currently selects Microsoft.Extensions.AI.OpenAI 10.6.0, which in turn selects OpenAI 2.10.0. Independent upgrades can compile successfully and still fail at run time if Azure.AI.Projects calls a constructor removed from a newer OpenAI client. After any package update, restore, inspect transitive versions, build and run the complete test suite before accepting the change.

Run the local Blazor and Aspire demonstration

The quickest path does not require a Microsoft 365 app registration.

az login --tenant '<foundry-home-tenant-guid>'
$env:AZURE_AI_PROJECT_ENDPOINT = 'https://<foundry-resource>.services.ai.azure.com/api/projects/<foundry-project>'
$env:AZURE_AI_MODEL_DEPLOYMENT_NAME = '<model-deployment-name>'
dotnet restore .\AgentPublishSample.slnx
dotnet run --project .\src\AgentPublishSample.AppHost

Open the HTTPS endpoint shown for gateway in the Aspire Dashboard. Local development without Bot credentials disables the Microsoft 365 channel endpoint authentication so that the Blazor diagnostic path can run. Do not expose /api/messages through a tunnel in this mode. The Blazor path admits the configured development tenant, and the model call still requires authorised access to the existing Foundry project.

The first start discovers Microsoft Learn MCP tools. A discovery or model authentication failure is surfaced through health, logs and traces rather than being reported as a successful agent response.

See Local development.

Prepare a local Microsoft 365 preview

Copy-Item .\env\.env.local.example .\env\.env.local
atk doctor
atk provision --env local
atk deploy --env local
atk preview --env local

Before provisioning, replace the example publisher URLs, tenant identifier and tunnel endpoint. Agents Toolkit writes generated identifiers to .env.local and the local secret to .env.local.user. Load BOT_ID, BOT_TENANT_ID and SECRET_BOT_PASSWORD into the shell that starts Aspire. Never copy the generated secret into source control or an Azure environment.

Prepare Azure Container Apps

The intended sequence is:

  1. review Container Apps deployment;
  2. inspect quotas, providers and permissions;
  3. set the non-secret azd environment values;
  4. compile Bicep and run subscription validation or what-if;
  5. obtain approval before azd provision or azd deploy;
  6. provision the ACA resources and deploy the gateway image;
  7. copy the generated BOT_ID, BOT_TENANT_ID and GATEWAY_DOMAIN values into the Agents Toolkit environment;
  8. validate and package the Microsoft 365 app.

This reference implementation performs no provider registration, provisioning or live deployment during build or validation. Each operator must complete the subscription, quota, permission and what-if checks in the deployment guide before provisioning.

Validate the repository

dotnet format .\AgentPublishSample.slnx --verify-no-changes --no-restore
dotnet build .\AgentPublishSample.slnx --configuration Release --no-restore
dotnet test .\AgentPublishSample.slnx --configuration Release --no-build --no-restore
dotnet list .\AgentPublishSample.slnx package --vulnerable --include-transitive
az bicep build --file .\infra\main.bicep --stdout | Out-Null
pwsh -NoProfile -File .\scripts\Test-AppPackage.ps1 -BuildPackage

For an Agents Toolkit environment whose placeholders have valid values:

pwsh -NoProfile -File .\scripts\Validate-AppPackage.ps1 -Environment dev

atk validate and atk package prepare an artefact. They do not certify it for the public store.

Microsoft 365 and Marketplace sequence

  1. Deploy and verify the Azure runtime.
  2. Render and validate the manifest using production identifiers.
  3. Install in a controlled tenant and complete functional, security and accessibility tests.
  4. Publish to the organisation catalogue where required.
  5. Enrol in the Microsoft 365 and Copilot Partner Center programme.
  6. Create an Apps and agents for Microsoft 365 and Copilot offer.
  7. Provide listing, privacy, support, responsible AI and certification evidence.
  8. Upload the validated package and respond to certification findings.
  9. Release only after operational readiness and publisher approval.

SaaS Fulfilment APIs, metering, licence enforcement and transaction landing pages are product concerns outside this sample.

Channel constraints

  • Microsoft supports custom engine agents from manifest schema 1.20. This minimum sample deliberately pins and validates schema 1.21.
  • The app package contains configuration and icons, not the .NET application.
  • Microsoft 365 Copilot does not pass image or file attachments to custom engine agents. The sample rejects all attachments before core dispatch.
  • A Teams file workflow requires separate storage, malware scanning, consent and authorisation design.
  • HTML responses, proactive notifications, message editing and pre-mention conversation history are not supported for this custom engine path.
  • Custom engine agents are not supported in Outlook, Word, Excel, PowerPoint or Edge.
  • Streaming must use one serialised stream for each turn.

Read Microsoft 365 publication for the complete capability and certification implications.

Documentation

Scope

The sample demonstrates the technical route from a portable ISV agent core to a Microsoft 365 custom engine agent package. It does not implement customer Graph access, product entitlement administration, durable conversation storage, file processing, human escalation, commerce APIs or automatic Partner Center approval.

Primary references

About

Thin M365 Agent SDK Sample

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages