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.
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]
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.
Production uses one user-assigned managed identity:
- Bicep creates the identity and attaches it to the Container App.
- Azure Bot Service registers its client ID with
msaAppTypeset toUserAssignedMSI. - Microsoft 365 Agents SDK uses
AuthType=UserManagedIdentity. - The same identity receives least-privilege access to the existing Foundry account and pulls the image from Azure Container Registry.
- The identity client ID becomes
BOT_IDin 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.
The following controls are independent:
- Channel authentication proves that the request was delivered through the configured Microsoft 365 and Bot Framework channel.
- Conversation tenant extraction reads the tenant from authenticated activity context. A header, query parameter or message body cannot establish tenant identity.
- ISV admission checks the trusted tenant against an onboarding allow-list. Authentication alone does not entitle a customer to use the service.
- 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.
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.
| 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.
An ISV releases two related artefacts:
- the gateway image, which contains the running .NET application and is deployed
to Azure Container Apps by
azd; and - 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
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.
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.
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.
- .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.
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.
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.AppHostOpen 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.
Copy-Item .\env\.env.local.example .\env\.env.local
atk doctor
atk provision --env local
atk deploy --env local
atk preview --env localBefore 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.
The intended sequence is:
- review Container Apps deployment;
- inspect quotas, providers and permissions;
- set the non-secret
azdenvironment values; - compile Bicep and run subscription validation or
what-if; - obtain approval before
azd provisionorazd deploy; - provision the ACA resources and deploy the
gatewayimage; - copy the generated
BOT_ID,BOT_TENANT_IDandGATEWAY_DOMAINvalues into the Agents Toolkit environment; - 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.
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 -BuildPackageFor an Agents Toolkit environment whose placeholders have valid values:
pwsh -NoProfile -File .\scripts\Validate-AppPackage.ps1 -Environment devatk validate and atk package prepare an artefact. They do not certify it for the
public store.
- Deploy and verify the Azure runtime.
- Render and validate the manifest using production identifiers.
- Install in a controlled tenant and complete functional, security and accessibility tests.
- Publish to the organisation catalogue where required.
- Enrol in the Microsoft 365 and Copilot Partner Center programme.
- Create an Apps and agents for Microsoft 365 and Copilot offer.
- Provide listing, privacy, support, responsible AI and certification evidence.
- Upload the validated package and respond to certification findings.
- Release only after operational readiness and publisher approval.
SaaS Fulfilment APIs, metering, licence enforcement and transaction landing pages are product concerns outside this sample.
- 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.
- Architecture
- Configuration
- Local development
- Container Apps deployment
- Microsoft 365 publication
- Commercial Marketplace checklist
- Security and multitenancy
- Observability
- Troubleshooting
- Infrastructure module reference
- Contributing
- Security policy
- MIT licence
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.