diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs index deafbb2ced..cb0e15dac8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs @@ -28,6 +28,23 @@ public static class A2AServerServiceCollectionExtensions /// The agent builder whose name identifies the agent. /// An optional callback to configure . /// The for chaining. + /// + /// + /// Trust model. The A2A contextId arrives from the wire + /// and is treated as a chain-resume identifier — not as an authorization + /// token. The contract carries no principal/owner + /// dimension, so when a persistent store is registered any caller who knows or + /// guesses another caller's contextId can resume that other caller's + /// persisted thread. Hosts that serve more than one user must compose a principal + /// dimension into the lookup key — typically by calling + /// UseClaimsBasedSessionIsolation(...) from + /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom + /// ). When no isolation provider is + /// registered, behavior is unchanged — the bare contextId is used as the + /// conversation identifier, which is appropriate for first-run / single-user / + /// prototyping scenarios but unsafe for multi-user hosts. + /// + /// public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBuilder, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(agentBuilder); @@ -46,6 +63,13 @@ public static class A2AServerServiceCollectionExtensions /// The name of the agent to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(builder); @@ -65,6 +89,13 @@ public static class A2AServerServiceCollectionExtensions /// The agent instance to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(builder); @@ -83,6 +114,13 @@ public static class A2AServerServiceCollectionExtensions /// The name of the agent to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(services); @@ -114,6 +152,13 @@ public static class A2AServerServiceCollectionExtensions /// The agent instance to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(services); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs index 948ecdca42..85fd00fb8b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs @@ -73,6 +73,26 @@ public static class AGUIEndpointRouteBuilderExtensions /// it will be used to persist conversation sessions across requests using the AG-UI thread ID as the /// conversation identifier. If no session store is registered, sessions are ephemeral (not persisted). /// + /// + /// Trust model. The AG-UI RunAgentInput.ThreadId arrives + /// from the wire and is treated as a chain-resume identifier — not as an + /// authorization token. The contract carries no + /// principal/owner dimension, so when a persistent store is registered any caller + /// who knows or guesses another caller's ThreadId can resume that other + /// caller's persisted thread. Hosts that serve more than one user must compose a + /// principal dimension into the lookup key. The recommended way is to wrap the + /// keyed in + /// , typically by calling + /// UseClaimsBasedSessionIsolation(...) from + /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom + /// ) and registering the store via the + /// WithSessionStore(...) / WithInMemorySessionStore(...) helpers on + /// so that the wrapper is applied. When no + /// isolation provider is registered, behavior is unchanged — the bare + /// ThreadId is used as the conversation identifier, which is appropriate + /// for first-run / single-user / prototyping scenarios but unsafe for + /// multi-user hosts. + /// /// public static IEndpointConventionBuilder MapAGUI( this IEndpointRouteBuilder endpoints, diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs index 1d5373e38a..7c0539fe51 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs @@ -11,9 +11,39 @@ namespace Microsoft.Agents.AI.Hosting; /// Defines the contract for storing and retrieving agent conversation threads. /// /// +/// /// Implementations of this interface enable persistent storage of conversation threads, /// allowing conversations to be resumed across HTTP requests, application restarts, /// or different service instances in hosted scenarios. +/// +/// +/// Trust model. The conversationId passed to +/// and typically originates +/// from the wire (for example, an AG-UI RunAgentInput.ThreadId or an A2A +/// contextId). It is a chain-resume identifier, not an authorization +/// token, and the (agent, conversationId) tuple carries no principal/owner +/// dimension. Hosts that serve more than one user from the same registered store must +/// therefore compose a principal dimension into the lookup key, otherwise any caller +/// who knows or guesses another caller's conversationId can resume +/// that other caller's persisted thread. The framework provides +/// as a decorator that rewrites +/// conversationId to include an isolation key resolved from a +/// (for example, the ASP.NET Core +/// ClaimsIdentitySessionIsolationKeyProvider wired up via +/// UseClaimsBasedSessionIsolation(...)). When no provider is registered, the +/// store behaves as a single-namespace persistence layer — appropriate for +/// single-user / first-run / prototyping scenarios but unsafe for multi-user hosts. +/// +/// +/// Implementer guidance. Implementations should treat +/// conversationId as opaque: do not parse it, do not impose length +/// or character-set constraints on it, and do not assume it round-trips to the value +/// the caller originally supplied (decorators such as +/// may rewrite it before forwarding). +/// Be aware that any logging, telemetry, or audit sink that surfaces +/// conversationId will also surface the isolation prefix when a +/// scoping decorator is in the chain. +/// /// public abstract class AgentSessionStore { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs index 9999527505..832c411977 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs @@ -24,6 +24,20 @@ namespace Microsoft.Agents.AI.Hosting; /// For production use with multiple instances or persistence across restarts, use a durable storage implementation /// such as Redis, SQL Server, or Azure Cosmos DB. /// +/// +/// Multi-user warning. This store keys threads by +/// (agent.Id, conversationId) only — it has no principal/owner dimension. When +/// the conversation identifier originates from the wire (for example, an AG-UI +/// RunAgentInput.ThreadId or an A2A contextId), any caller who knows +/// or guesses another caller's identifier can resume that other caller's persisted +/// thread. Multi-user hosts must wrap this store in +/// (typically by calling +/// UseClaimsBasedSessionIsolation(...) from +/// Microsoft.Agents.AI.Hosting.AspNetCore or by registering a custom +/// ) so that the conversation namespace is +/// scoped per principal. See the trust-model remarks on +/// for the full background. +/// /// public sealed class InMemoryAgentSessionStore : AgentSessionStore {