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
2 changes: 1 addition & 1 deletion docs/specs/003-dotnet-hosting-protocol-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ parsing a structured payload into a typed record), without coupling the holder t
- Authorize and bind the candidate id to the authenticated principal/tenant before using it as an
`AgentSessionStore` key or a workflow checkpoint session id.
- For multi-user hosts, wrap the store with `IsolationKeyScopedAgentSessionStore` (for example via
`UseClaimsBasedSessionIsolation(...)`), so the session namespace is scoped per principal.
`UseClaimsBasedAgentIsolation(...)`), so the session namespace is scoped per principal.
- Persist session/checkpoint state only after the run or stream has completed.

## E2E Code Samples
Expand Down
2 changes: 1 addition & 1 deletion dotnet/samples/02-agents/AGUI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ dotnet run

`ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace.

If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedAgentIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.

## Troubleshooting

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
builder.Services.AddAGUIServer();

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
builder.Services.AddAGUIServer();

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
builder.Services.AddAGUIServer();

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
builder.Services.AddAGUIServer();

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
builder.WebHost.UseUrls("http://localhost:8888");

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ You specialize in handling queries related to logistics.
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided");
}

// IMPORTANT: In production, register a SessionIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// IMPORTANT: In production, register an AgentIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// Without this, contextId/taskId alone are the lookup keys — any caller who knows them can access another caller's data.
// Example using claims-based identity:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

// By default, NoopAgentSessionStore is used — sessions are not persisted across requests.
// To enable multi-turn conversations, register a session store explicitly, e.g.:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
builder.Services.AddAGUIServer();

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@
]);

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

// Register the agent with the host and configure it to use an in-memory session store
// so that conversation state is maintained across requests. In production, you may want to use a persistent session store.
Expand Down
4 changes: 2 additions & 2 deletions dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
builder.Services.AddAGUIServer();

// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

WebApplication app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();

// IMPORTANT: In production, register a SessionIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// IMPORTANT: In production, register an AgentIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// Without this, contextId/taskId alone are the lookup keys — any caller who knows them can access another caller's data.
// Example using claims-based identity:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

// By default, NoopAgentSessionStore is used — sessions are not persisted across requests.
// To enable multi-turn conversations, register a session store explicitly, e.g.:
Expand Down Expand Up @@ -157,10 +157,10 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
pirateAgentBuilder.AddA2AServer();
knightsKnavesAgentBuilder.AddA2AServer();

// IMPORTANT: In production, register a SessionIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// IMPORTANT: In production, register an AgentIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// Without this, contextId/taskId alone are the lookup keys — any caller who knows them can access another caller's data.
// Example using claims-based identity:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ public static class A2AServerServiceCollectionExtensions
/// so when a persistent store is registered any caller who knows or guesses another
/// caller's <c>contextId</c> or <c>taskId</c> can access that other caller's data.
/// Hosts that serve more than one user must compose a principal dimension into the
/// lookup key — typically by calling <c>UseClaimsBasedSessionIsolation(...)</c> from
/// lookup key — typically by calling <c>UseClaimsBasedAgentIsolation(...)</c> from
/// <c>Microsoft.Agents.AI.Hosting.AspNetCore</c> (or by registering a custom
/// <see cref="SessionIsolationKeyProvider"/>). When a <see cref="SessionIsolationKeyProvider"/>
/// <see cref="AgentIsolationKeyProvider"/>). When an <see cref="AgentIsolationKeyProvider"/>
/// is registered, both the session store and the task store are automatically wrapped
/// with tenant-scoped isolation. When no isolation provider is registered, behavior
/// is unchanged — the bare identifiers are used directly, which is appropriate for
Expand Down Expand Up @@ -68,8 +68,8 @@ public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBui
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
/// for guidance on multi-user hosts (the wire <c>contextId</c> and <c>taskId</c>
/// are chain-resume identifiers, not authorization tokens; multi-user hosts must
/// compose a principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or
/// a custom <see cref="SessionIsolationKeyProvider"/>).
/// compose a principal dimension via <c>UseClaimsBasedAgentIsolation(...)</c> or
/// a custom <see cref="AgentIsolationKeyProvider"/>).
/// </remarks>
public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
Expand All @@ -94,8 +94,8 @@ public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
/// for guidance on multi-user hosts (the wire <c>contextId</c> and <c>taskId</c>
/// are chain-resume identifiers, not authorization tokens; multi-user hosts must
/// compose a principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or
/// a custom <see cref="SessionIsolationKeyProvider"/>).
/// compose a principal dimension via <c>UseClaimsBasedAgentIsolation(...)</c> or
/// a custom <see cref="AgentIsolationKeyProvider"/>).
/// </remarks>
public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
Expand All @@ -119,8 +119,8 @@ public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
/// for guidance on multi-user hosts (the wire <c>contextId</c> and <c>taskId</c>
/// are chain-resume identifiers, not authorization tokens; multi-user hosts must
/// compose a principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or
/// a custom <see cref="SessionIsolationKeyProvider"/>).
/// compose a principal dimension via <c>UseClaimsBasedAgentIsolation(...)</c> or
/// a custom <see cref="AgentIsolationKeyProvider"/>).
/// </remarks>
public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
Expand Down Expand Up @@ -157,8 +157,8 @@ public static IServiceCollection AddA2AServer(this IServiceCollection services,
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
/// for guidance on multi-user hosts (the wire <c>contextId</c> and <c>taskId</c>
/// are chain-resume identifiers, not authorization tokens; multi-user hosts must
/// compose a principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or
/// a custom <see cref="SessionIsolationKeyProvider"/>).
/// compose a principal dimension via <c>UseClaimsBasedAgentIsolation(...)</c> or
/// a custom <see cref="AgentIsolationKeyProvider"/>).
/// </remarks>
public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action<A2AServerRegistrationOptions>? configureOptions = null)
{
Expand All @@ -180,7 +180,7 @@ public static IServiceCollection AddA2AServer(this IServiceCollection services,

private static A2AServer CreateA2AServer(IServiceProvider serviceProvider, AIAgent agent, A2AServerRegistrationOptions? options)
{
var isolationKeyProvider = serviceProvider.GetService<SessionIsolationKeyProvider>();
var isolationKeyProvider = serviceProvider.GetService<AgentIsolationKeyProvider>();

var agentHandler = serviceProvider.GetKeyedService<IAgentHandler>(agent.Name);
if (agentHandler is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Agents.AI.Hosting.A2A;

/// <summary>
/// A delegating <see cref="ITaskStore"/> that scopes task keys by an isolation key
/// provided by a <see cref="SessionIsolationKeyProvider"/>, ensuring that tasks are isolated
/// provided by an <see cref="AgentIsolationKeyProvider"/>, ensuring that tasks are isolated
/// per logical partition (e.g., user, tenant, or composite key).
/// </summary>
/// <remarks>
Expand All @@ -28,15 +28,15 @@ namespace Microsoft.Agents.AI.Hosting.A2A;
public sealed class IsolationKeyScopedTaskStore : ITaskStore
{
private readonly ITaskStore _innerStore;
private readonly SessionIsolationKeyProvider? _keyProvider;
private readonly AgentIsolationKeyProvider? _keyProvider;
private readonly bool _strict;

/// <summary>
/// Initializes a new instance of the <see cref="IsolationKeyScopedTaskStore"/> class.
/// </summary>
/// <param name="innerStore">The underlying <see cref="ITaskStore"/> to delegate to.</param>
/// <param name="keyProvider">
/// The <see cref="SessionIsolationKeyProvider"/> used to retrieve the isolation key for the current context.
/// The <see cref="AgentIsolationKeyProvider"/> used to retrieve the isolation key for the current context.
/// </param>
/// <param name="strict">
/// When <see langword="true"/>, an <see cref="InvalidOperationException"/> is thrown if the isolation key
Expand All @@ -45,7 +45,7 @@ public sealed class IsolationKeyScopedTaskStore : ITaskStore
/// <exception cref="ArgumentNullException"><paramref name="innerStore"/> is <see langword="null"/>.</exception>
public IsolationKeyScopedTaskStore(
ITaskStore innerStore,
SessionIsolationKeyProvider? keyProvider,
AgentIsolationKeyProvider? keyProvider,
bool strict)
{
ArgumentNullException.ThrowIfNull(innerStore);
Expand Down Expand Up @@ -130,12 +130,12 @@ public async Task<ListTasksResponse> ListTasksAsync(ListTasksRequest request, Ca
private async ValueTask<string?> GetIsolationKeyAsync(CancellationToken cancellationToken)
{
string? key = this._keyProvider != null
? await this._keyProvider.GetSessionIsolationKeyAsync(cancellationToken).ConfigureAwait(false)
? await this._keyProvider.GetIsolationKeyAsync(cancellationToken).ConfigureAwait(false)
: null;

if (this._strict && key == null)
{
throw new InvalidOperationException("Session isolation key is required but was not provided by the configured SessionIsolationKeyProvider.");
throw new InvalidOperationException("Agent isolation key is required but was not provided by the configured AgentIsolationKeyProvider.");
}

return key;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ public static IEndpointConventionBuilder MapAGUIServer(
/// principal dimension into the lookup key. The recommended way is to wrap the
/// keyed <see cref="AgentSessionStore"/> in
/// <see cref="IsolationKeyScopedAgentSessionStore"/>, typically by calling
/// <c>UseClaimsBasedSessionIsolation(...)</c> from
/// <c>UseClaimsBasedAgentIsolation(...)</c> from
/// <c>Microsoft.Agents.AI.Hosting.AspNetCore</c> (or by registering a custom
/// <see cref="SessionIsolationKeyProvider"/>) and registering the store via the
/// <see cref="AgentIsolationKeyProvider"/>) and registering the store via the
/// <c>WithSessionStore(...)</c> / <c>WithInMemorySessionStore(...)</c> helpers on
/// <see cref="IHostedAgentBuilder"/> so that the wrapper is applied. When no
/// isolation provider is registered, behavior is unchanged — the bare
Expand All @@ -113,7 +113,7 @@ public static IEndpointConventionBuilder MapAGUIServer(
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(aiAgent.Name);

// Ensure that we have an IsolationKeyScopedAgentSessionStore registered.
var isolationKeyProvider = endpoints.ServiceProvider.GetService<SessionIsolationKeyProvider>();
var isolationKeyProvider = endpoints.ServiceProvider.GetService<AgentIsolationKeyProvider>();
if (agentSessionStore?.GetService<IsolationKeyScopedAgentSessionStore>() is null)
{
agentSessionStore ??= new NoopAgentSessionStore();
Expand Down
Loading
Loading