Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@ Both `Agent365.Observability.OtelWrite` (Delegated) and `Agent365.Observability.

## [Unreleased]

### Breaking Changes

- **OBS exports always use `/observabilityService`** — `Microsoft.Agents.A365.Observability.Runtime`
now sends every Agent 365 telemetry export to the S2S OTLP route
`/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1`.
`Agent365ExporterOptions.UseS2SEndpoint` is obsolete and ignored, even when `false`;
there is no fallback to `/observability`.
- **OBS export requires a configured app-only resolver** — `TokenResolver` and
`ContextualTokenResolver` must return the final OBS app-only token for the exporting
agent and tenant. Missing resolvers fail exporter construction; empty tokens or resolver
failures fail the export batch before sending a request. Resolvers are invoked once per
tenant/agent identity group in each export batch, so they should cache tokens per agent and
tenant and refresh them near expiry. Workload OBO/MCP/Graph auth is
unchanged. `EnvironmentUtils.GetObservabilityAuthenticationScope()` now returns the OBS
`/.default` scope for app-only S2S export instead of the delegated
`Agent365.Observability.OtelWrite` scope.
- **Delegated hosting OBS token acquisition is removed** —
`AgenticTokenCache.RegisterObservability(..., AgenticTokenStruct, ...)` is obsolete with
`error: true`, and `AgenticTokenStruct` construction is obsolete with `error: true`.
Use `ObservabilityTokenResolver` and `AgenticTokenCache.RefreshObservabilityToken(...)`
for app-only token acquisition. `AddAgenticTracingExporter` now registers
`IExporterTokenCache<ObservabilityTokenResolver>` instead of
`IExporterTokenCache<AgenticTokenStruct>`.

### Added
- **Microsoft.Agents.A365.Tooling** - V1/V2 per-audience token support for MCP servers
- `MCPServerConfig` extended with `audience`, `scope`, `publisher`, and `Headers` fields
Expand Down
292 changes: 236 additions & 56 deletions src/Observability/Hosting/Caching/AgenticTokenCache.cs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Observability/Hosting/Caching/AgenticTokenStruct.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class AgenticTokenStruct
/// <param name="authHandlerName"></param>
/// <param name="connectionName"></param>
/// <exception cref="ArgumentNullException"></exception>
[Obsolete("Delegated OBS token acquisition has been removed. Use ObservabilityTokenResolver with app-only tokens instead.", error: true)]
public AgenticTokenStruct(
UserAuthorization userAuthorization,
ITurnContext turnContext,
Expand Down
8 changes: 6 additions & 2 deletions src/Observability/Hosting/Caching/IExporterTokenCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ namespace Microsoft.Agents.A365.Observability.Hosting.Caching
public interface IExporterTokenCache<T> where T : class
{
/// <summary>
/// Registers (idempotent) a credential to be used for observability token acquisition.
/// Registers a credential or resolver used for app-only observability token acquisition.
/// Whether a repeated registration for the same agent and tenant replaces the existing one is
/// implementation-specific: <see cref="AgenticTokenCache"/> keeps the first registration and
/// <see cref="ServiceTokenCache"/> replaces it.
/// </summary>
void RegisterObservability(string agentId, string tenantId, T tokenGenerator, string[] observabilityScopes);

/// <summary>
/// Returns an observability token (cached inside the credential) or null on failure/not registered.
/// Returns an observability token or <c>null</c> when not registered.
/// Implementations that acquire a new token may propagate resolver failures to the caller.
/// </summary>
Task<string?> GetObservabilityToken(string agentId, string tenantId);
}
Expand Down
17 changes: 17 additions & 0 deletions src/Observability/Hosting/Caching/ObservabilityTokenResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Threading.Tasks;

namespace Microsoft.Agents.A365.Observability.Hosting.Caching
{
/// <summary>
/// Resolves an app-only OBS token for the exporting agent and tenant.
/// The resolver must not perform delegated, OBO, or user_fic authentication for OBS export.
/// </summary>
/// <param name="agentId">The exporting agent identifier.</param>
/// <param name="tenantId">The tenant identifier.</param>
/// <param name="observabilityScopes">The OBS token scopes to request.</param>
/// <returns>The final OBS access token for the exporting agent.</returns>
public delegate Task<string?> ObservabilityTokenResolver(string agentId, string tenantId, string[] observabilityScopes);
}
76 changes: 53 additions & 23 deletions src/Observability/Hosting/Caching/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
# ServiceTokenCache - Token Expiration and Invalidation
# OBS Token Caches - Token Expiration and Invalidation

## Overview

Agent 365 OBS export is S2S-only. Exporters always send traces to
`/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1`
and require app-only OBS tokens for the exporting agent identity.

`ServiceTokenCache` is a reference implementation of `IExporterTokenCache<string>` that provides secure token caching with built-in expiration and invalidation features for observability exporters.
`AgenticTokenCache` stores app-only tokens resolved by `ObservabilityTokenResolver`; the
former delegated `AgenticTokenStruct` registration is obsolete and no longer performs OBO
or TurnContext token exchange for OBS. `RegisterObservability` is idempotent:
first registration wins and repeated calls do not replace the resolver or clear a cached
token. Use `RefreshObservabilityToken` to replace the resolver used by future refreshes.

Resolvers must validate the token they return: accept `idtyp=app`, or, when `idtyp` is
absent, a non-empty `roles` array or a non-empty `oid` equal to `sub`; reject any other
`idtyp` and reject any token containing `scp`. They must also verify the OBS audience
(`api://9b975845-388f-4429-889e-eab1ef63949c`) and token lifetime.

## Features

Expand Down Expand Up @@ -41,6 +55,36 @@ cache.RegisterObservability(
var token = cache.GetObservabilityToken("my-agent", "my-tenant");
```

### App-only Resolver Cache

```csharp
var cache = new AgenticTokenCache();

ObservabilityTokenResolver resolver = async (agentId, tenantId, scopes) =>
{
var token = await AcquireAppOnlyObsTokenAsync(agentId, tenantId, scopes);
ValidateAppOnlyObsToken(token);
return token;
};

await cache.RefreshObservabilityToken("my-agent", "my-tenant", resolver);
var token = await cache.GetObservabilityToken("my-agent", "my-tenant");
```

`RefreshObservabilityToken` returns the cached token without calling the resolver while the
cached token is still usable. It propagates acquisition failures and clears stale cached
token state when the resolver fails or returns an empty token. Call it from the exporter's
`TokenResolver` or catch errors on the request path; the exporter will fail the batch
without attempting a delegated fallback. JWT tokens are refreshed near `exp`; opaque tokens
without an `exp` claim use a one-hour fallback max age from acquisition. The three-argument
`RefreshObservabilityToken` overload passes the default app-only OBS scope
`api://9b975845-388f-4429-889e-eab1ef63949c/.default` to the resolver unless
`A365_OBSERVABILITY_SCOPE_OVERRIDE` is set.

Concurrent refreshes for the same agent and tenant are serialized, so callers share one
acquisition. The automatic cleanup and `RemoveExpiredTokens` clear expired token values but
keep the resolver registration, so the next `GetObservabilityToken` call acquires a new token.

### Custom Default Expiration

```csharp
Expand Down Expand Up @@ -186,16 +230,10 @@ public class TokenCleanupService : BackgroundService

- **Never log tokens**: Avoid logging the actual token values
- **Use appropriate expiration**: Match expiration time with your security requirements
- **Invalidate on logout**: Call `InvalidateToken` when a user logs out
- **Keep workload auth separate**: OBS export tokens are app-only and independent of MCP/Graph/OBO tokens
- **Clear cache on security events**: Use `InvalidateAll()` in response to security events

```csharp
// On user logout
public void OnUserLogout(string agentId, string tenantId)
{
cache.InvalidateToken(agentId, tenantId);
}

// On security breach detection
public void OnSecurityBreach()
{
Expand Down Expand Up @@ -250,25 +288,17 @@ Parallel.For(0, 100, i =>

## Migration from Previous Version

If you're upgrading from a previous version without expiration support, no code changes are required. The default behavior maintains backward compatibility:

```csharp
// Old code - still works with default 1-hour expiration
var cache = new ServiceTokenCache();
cache.RegisterObservability("agent", "tenant", "token", scopes);
var token = cache.GetObservabilityToken("agent", "tenant");
```

To opt-in to custom expiration:
If you're upgrading from a delegated OBS token flow, replace `AgenticTokenStruct` and
`RegisterObservability(..., AgenticTokenStruct, ...)` with an app-only
`ObservabilityTokenResolver`:

```csharp
// New code - with custom expiration
var cache = new ServiceTokenCache(TimeSpan.FromMinutes(30));
cache.RegisterObservability("agent", "tenant", "token", scopes, TimeSpan.FromMinutes(10));
await cache.RefreshObservabilityToken("agent", "tenant", resolver);
var token = await cache.GetObservabilityToken("agent", "tenant");
```

## See Also

- [IExporterTokenCache Interface](../Core/Caching/IExporterTokenCache.cs)
- [AgenticTokenCache](../Core/Caching/AgenticTokenCache.cs) - Alternative implementation for agentic scenarios
- [IExporterTokenCache Interface](IExporterTokenCache.cs)
- [AgenticTokenCache](AgenticTokenCache.cs) - App-only resolver cache
- [Observability SDK Documentation](../README.md)
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@
<PackageReference Include="Microsoft.AspNetCore.Hosting" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.A365.Observability.Hosting.Tests" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public static class ObservabilityServiceCollectionExtensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddAgenticTracingExporter(this IServiceCollection services, string? clusterCategory = "production")
{
services.AddSingleton<IExporterTokenCache<AgenticTokenStruct>, AgenticTokenCache>();
services.AddSingleton<IExporterTokenCache<ObservabilityTokenResolver>, AgenticTokenCache>();

services.AddSingleton(sp =>
{
var cache = sp.GetRequiredService<IExporterTokenCache<AgenticTokenStruct>>();
var cache = sp.GetRequiredService<IExporterTokenCache<ObservabilityTokenResolver>>();
return new Agent365ExporterOptions
{
ClusterCategory = clusterCategory ?? "production",
Expand Down Expand Up @@ -51,8 +51,7 @@ public static IServiceCollection AddServiceTracingExporter(this IServiceCollecti
return new Agent365ExporterOptions
{
ClusterCategory = clusterCategory ?? "production",
TokenResolver = async (agentId, tenantId) => await cache.GetObservabilityToken(agentId, tenantId).ConfigureAwait(false),
UseS2SEndpoint = true // Service-to-service uses S2S endpoint
TokenResolver = async (agentId, tenantId) => await cache.GetObservabilityToken(agentId, tenantId).ConfigureAwait(false)
};
});

Expand Down
14 changes: 14 additions & 0 deletions src/Observability/Hosting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

The Hosting package provides ETW (Event Tracing for Windows) integration for the Microsoft Agent 365 Observability SDK. This package enables high-performance event tracing on Windows platforms for production monitoring scenarios.

## OBS Token Caching

OBS export is S2S-only. Hosting helpers must provide app-only tokens for the exporting
agent identity; delegated TurnContext/UserAuthorization exchange is no longer used for OBS.
`AgenticTokenCache.RegisterObservability(..., AgenticTokenStruct, ...)` and
`AgenticTokenStruct` construction are obsolete compile-time errors. Register or refresh an
`ObservabilityTokenResolver` instead, then wire `Agent365ExporterOptions.TokenResolver` to
`GetObservabilityToken`.

The resolver is responsible for acquiring and validating the final OBS token. It must reject
delegated `scp` tokens, accept app-only tokens (`idtyp=app`, or no `idtyp` with non-empty
`roles` or `oid == sub`), verify the OBS audience, and ensure the token is not expired.
Workload MCP/Graph/OBO authentication remains separate and unchanged.

## Installation

```bash
Expand Down
24 changes: 18 additions & 6 deletions src/Observability/Hosting/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,25 +119,37 @@ var sourceMetadataPairs = turnContext.GetSourceMetadataBaggagePairs();

### Token Caching

Token caches for managing authentication tokens used by telemetry exporters.
Token caches for managing app-only authentication tokens used by telemetry exporters.
OBS export always uses the S2S OTLP route and never uses TurnContext or delegated
authorization to acquire OBS tokens.

**IExporterTokenCache Interface:**

```csharp
public interface IExporterTokenCache
public interface IExporterTokenCache<T> where T : class
{
Task<string?> GetTokenAsync(string resource, CancellationToken cancellationToken);
Task SetTokenAsync(string resource, string token, DateTimeOffset expiry, CancellationToken cancellationToken);
void RegisterObservability(string agentId, string tenantId, T tokenGenerator, string[] observabilityScopes);
Task<string?> GetObservabilityToken(string agentId, string tenantId);
}
```

**AgenticTokenCache:**

Caches tokens for agentic operations, using the user's delegated identity.
Caches app-only OBS tokens per `(agentId, tenantId)` using an `ObservabilityTokenResolver`.
The previous `AgenticTokenStruct`/`UserAuthorization` registration is obsolete with
`error: true`; typed callers must migrate, and dynamic callers do not trigger a delegated
exchange. Use `RefreshObservabilityToken(agentId, tenantId, tokenResolver, scopes)` to
refresh at export time and let acquisition failures propagate to the exporter.

**ServiceTokenCache:**

Caches tokens for service-to-service operations, using the application identity.
Caches already-acquired app-only tokens for service-to-service operations.

Resolvers must validate the final OBS token before caching it: accept `idtyp=app`, or,
when `idtyp` is absent, a non-empty `roles` array or a non-empty `oid` equal to `sub`;
reject any other `idtyp` and any `scp` claim. Resolvers must also verify the OBS audience
(`api://9b975845-388f-4429-889e-eab1ef63949c`) and token lifetime. Workload MCP/Graph/OBO
authorization is separate and unchanged.

### BaggageBuilderExtensions

Expand Down
4 changes: 4 additions & 0 deletions src/Observability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

The Microsoft Agent 365 Observability SDK provides comprehensive monitoring, tracing, and diagnostics capabilities for AI agent applications. This module enables developers to gain deep insights into agent behavior, performance, and execution patterns through industry-standard observability tools.

Agent 365 OBS export uses the S2S OTLP endpoint only and requires an app-only OBS token for
the exporting agent identity. The SDK does not acquire delegated OBS tokens or fall back to
the delegated `/observability` route.

## Overview

Building production-ready AI agents requires robust observability to understand agent behavior, diagnose issues, and optimize performance. This module provides:
Expand Down
8 changes: 3 additions & 5 deletions src/Observability/Runtime/Common/EnvironmentUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Common
/// </summary>
public class EnvironmentUtils
{
private const string ProdObservabilityScope = "api://9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite";
private const string ProdObservabilityScope = "api://9b975845-388f-4429-889e-eab1ef63949c/.default";
private const string ProdObservabilityClusterCategory = "prod";
private const string DevelopmentEnvironmentName = "development";

/// <summary>
/// Returns the scope for authenticating to the observability service based on the current environment.
/// Returns the app-only OBS resource scope for authenticating to the observability service.
/// </summary>
/// <returns>The authentication scope.</returns>
public static string[] GetObservabilityAuthenticationScope()
Expand All @@ -24,7 +24,7 @@ public static string[] GetObservabilityAuthenticationScope()
}

/// <summary>
/// [Deprecated] Returns the scope for authenticating to the observability service based on the cluster category.
/// [Deprecated] Returns the app-only OBS resource scope for authenticating to the observability service.
/// </summary>
/// <param name="clusterCategory">Cluster category (deprecated, defaults to production).</param>
/// <returns>The authentication scope.</returns>
Expand Down Expand Up @@ -77,5 +77,3 @@ private static string GetCurrentEnvironment()
}
}
}


20 changes: 20 additions & 0 deletions src/Observability/Runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

The Runtime package provides runtime components for the Microsoft Agent 365 Observability SDK, including exporters, tracing utilities, DTOs, and scope management.

## Agent 365 Exporter Authentication

Agent 365 OBS export is S2S-only. The exporter always posts OTLP traces to
`https://{endpoint}/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1`;
the legacy `Agent365ExporterOptions.UseS2SEndpoint` switch is obsolete and ignored.

Configure either `TokenResolver` or `ContextualTokenResolver` to return the final app-only
OBS token for the exporting agent and tenant. The default app-only OBS scope is
`api://9b975845-388f-4429-889e-eab1ef63949c/.default`. The SDK never reads a delegated
request token, never performs OBO/user_fic authentication for OBS export, and never falls
back to `/observability` on 401, 403, or 404. The resolver is invoked once per tenant/agent
identity group in each export batch, so a batch that contains several identities invokes it
several times. Cache tokens per agent and tenant and refresh only near expiry.

Resolvers must validate the returned token before handing it to the exporter: accept
`idtyp=app`, or, when `idtyp` is absent, a non-empty `roles` array or a non-empty `oid`
equal to `sub`; reject any other `idtyp` and any token with an `scp` claim. Also verify the
audience is the Agent 365 OBS resource (`api://9b975845-388f-4429-889e-eab1ef63949c`) and
that the token is not expired.

## Installation

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public Agent365Exporter(
_options = options ?? throw new ArgumentNullException(nameof(options));

if (_options.TokenResolver == null && _options.ContextualTokenResolver == null)
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver or ContextualTokenResolver must be provided.");
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver or ContextualTokenResolver must provide an app-only OBS token.");

_httpClient = httpClient ?? HttpClientFactory.CreateWithTimeout(options.ExporterTimeoutMilliseconds);
_resource = resource ?? ResourceBuilder.CreateEmpty().Build();
Expand Down Expand Up @@ -87,4 +87,3 @@ public override ExportResult Export(in Batch<Activity> batch)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public Agent365ExporterAsync(
this._options = options ?? throw new ArgumentNullException(nameof(options));

if (_options.TokenResolver == null && _options.ContextualTokenResolver == null)
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver or ContextualTokenResolver must be provided.");
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver or ContextualTokenResolver must provide an app-only OBS token.");

this._httpClient = httpClient ?? HttpClientFactory.CreateWithTimeout(options.ExporterTimeoutMilliseconds);
this._resource = resource ?? ResourceBuilder.CreateEmpty().Build();
Expand Down Expand Up @@ -92,4 +92,3 @@ await _core.ExportBatchCoreAsync(
}
}
}

Loading
Loading