diff --git a/docs/specs/003-dotnet-hosting-protocol-helpers.md b/docs/specs/003-dotnet-hosting-protocol-helpers.md index 158d0215195..68a3a4353f3 100644 --- a/docs/specs/003-dotnet-hosting-protocol-helpers.md +++ b/docs/specs/003-dotnet-hosting-protocol-helpers.md @@ -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 diff --git a/dotnet/samples/02-agents/AGUI/README.md b/dotnet/samples/02-agents/AGUI/README.md index 004b9f3b4e4..b0e724bf14e 100644 --- a/dotnet/samples/02-agents/AGUI/README.md +++ b/dotnet/samples/02-agents/AGUI/README.md @@ -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 diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs index 407bf1c4e82..d783b8535f1 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs @@ -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(); diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs index 83cf2500c9d..6aacb1b3320 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs @@ -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(); diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs index 407bf1c4e82..d783b8535f1 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs @@ -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(); diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs index 570c02b8bbb..b865d56161f 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -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(); diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs index 2232311630c..b323406974b 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs @@ -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(); diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs index a76e779461f..a2834fb69d6 100644 --- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs @@ -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.: diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs index bc984dc1d6c..49100e42d84 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs @@ -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(); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index 908c4f87443..e3de957c1e7 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -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. diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs index 92c8c83307e..6f888b9dc36 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs @@ -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(); diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 8fd96ef3bdd..b0713fc6abb 100644 --- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -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.: @@ -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(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs index a11cd6ba86c..556a0d931a3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs @@ -37,9 +37,9 @@ public static class A2AServerServiceCollectionExtensions /// so when a persistent store is registered any caller who knows or guesses another /// caller's contextId or taskId 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 UseClaimsBasedSessionIsolation(...) from + /// lookup key — typically by calling UseClaimsBasedAgentIsolation(...) from /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom - /// ). When a + /// ). When an /// 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 @@ -68,8 +68,8 @@ public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBui /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId and taskId /// are chain-resume identifiers, not authorization tokens; multi-user hosts must - /// compose a principal dimension via UseClaimsBasedSessionIsolation(...) or - /// a custom ). + /// compose a principal dimension via UseClaimsBasedAgentIsolation(...) or + /// a custom ). /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action? configureOptions = null) { @@ -94,8 +94,8 @@ public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId and taskId /// are chain-resume identifiers, not authorization tokens; multi-user hosts must - /// compose a principal dimension via UseClaimsBasedSessionIsolation(...) or - /// a custom ). + /// compose a principal dimension via UseClaimsBasedAgentIsolation(...) or + /// a custom ). /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action? configureOptions = null) { @@ -119,8 +119,8 @@ public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId and taskId /// are chain-resume identifiers, not authorization tokens; multi-user hosts must - /// compose a principal dimension via UseClaimsBasedSessionIsolation(...) or - /// a custom ). + /// compose a principal dimension via UseClaimsBasedAgentIsolation(...) or + /// a custom ). /// public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action? configureOptions = null) { @@ -157,8 +157,8 @@ public static IServiceCollection AddA2AServer(this IServiceCollection services, /// See the trust-model remarks on /// for guidance on multi-user hosts (the wire contextId and taskId /// are chain-resume identifiers, not authorization tokens; multi-user hosts must - /// compose a principal dimension via UseClaimsBasedSessionIsolation(...) or - /// a custom ). + /// compose a principal dimension via UseClaimsBasedAgentIsolation(...) or + /// a custom ). /// public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action? configureOptions = null) { @@ -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(); + var isolationKeyProvider = serviceProvider.GetService(); var agentHandler = serviceProvider.GetKeyedService(agent.Name); if (agentHandler is null) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/IsolationKeyScopedTaskStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/IsolationKeyScopedTaskStore.cs index d70cfe0fd44..9610e6f6982 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/IsolationKeyScopedTaskStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/IsolationKeyScopedTaskStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.Agents.AI.Hosting.A2A; /// /// A delegating that scopes task keys by an isolation key -/// provided by a , ensuring that tasks are isolated +/// provided by an , ensuring that tasks are isolated /// per logical partition (e.g., user, tenant, or composite key). /// /// @@ -28,7 +28,7 @@ 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; /// @@ -36,7 +36,7 @@ public sealed class IsolationKeyScopedTaskStore : ITaskStore /// /// The underlying to delegate to. /// - /// The used to retrieve the isolation key for the current context. + /// The used to retrieve the isolation key for the current context. /// /// /// When , an is thrown if the isolation key @@ -45,7 +45,7 @@ public sealed class IsolationKeyScopedTaskStore : ITaskStore /// is . public IsolationKeyScopedTaskStore( ITaskStore innerStore, - SessionIsolationKeyProvider? keyProvider, + AgentIsolationKeyProvider? keyProvider, bool strict) { ArgumentNullException.ThrowIfNull(innerStore); @@ -130,12 +130,12 @@ public async Task ListTasksAsync(ListTasksRequest request, Ca private async ValueTask 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; 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 ea1f6196bfb..348d4a86625 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs @@ -91,9 +91,9 @@ public static IEndpointConventionBuilder MapAGUIServer( /// principal dimension into the lookup key. The recommended way is to wrap the /// keyed in /// , typically by calling - /// UseClaimsBasedSessionIsolation(...) from + /// UseClaimsBasedAgentIsolation(...) from /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom - /// ) and registering the store via the + /// ) 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 @@ -113,7 +113,7 @@ public static IEndpointConventionBuilder MapAGUIServer( var agentSessionStore = endpoints.ServiceProvider.GetKeyedService(aiAgent.Name); // Ensure that we have an IsolationKeyScopedAgentSessionStore registered. - var isolationKeyProvider = endpoints.ServiceProvider.GetService(); + var isolationKeyProvider = endpoints.ServiceProvider.GetService(); if (agentSessionStore?.GetService() is null) { agentSessionStore ??= new NoopAgentSessionStore(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentityAgentIsolationKeyProvider.cs similarity index 72% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs rename to dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentityAgentIsolationKeyProvider.cs index 7cb1022bae3..39f54a7c046 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentityAgentIsolationKeyProvider.cs @@ -11,63 +11,63 @@ namespace Microsoft.Agents.AI.Hosting; /// -/// A that extracts the session isolation key from a claim -/// in the current user's identity, as provided by ASP.NET Core's . +/// An that extracts an isolation key for agent-owned resources +/// from a claim in the current user's identity, as provided by ASP.NET Core's . /// /// /// -/// This provider is suitable for ASP.NET Core web applications where session isolation is based on -/// authenticated user identity. It reads a specified claim type (e.g., name, email, or a custom identifier) +/// This provider is suitable for ASP.NET Core web applications where agent-owned resources are partitioned +/// by authenticated user identity. It reads a specified claim type (e.g., name, email, or a custom identifier) /// from the ambient . /// /// -/// Security warning: The configured +/// Security warning: The configured /// must uniquely identify the principal within the served population. Display names, usernames, email /// aliases, and other mutable or non-unique claims are unsafe isolation keys unless the /// host can prove their uniqueness across all callers: two distinct principals that share the same value -/// would receive the same isolation key and could read or overwrite one another's persisted sessions. +/// would receive the same isolation key and could read or overwrite one another's persisted data. /// The default claim type is , a stable unique subject identifier /// that is typically populated from the OpenID Connect sub claim via the default JWT inbound claim /// mapping (note that this differs from Entra's object identifier oid claim; override -/// if you need oid or your +/// if you need oid or your /// provider maps a different claim). /// /// /// If the is unavailable, the user is not authenticated, or the specified claim -/// is missing, the provider returns . The consuming -/// will then enforce strict or pass-through behavior based on its configuration. +/// is missing, the provider returns . Consuming stores then enforce strict or +/// pass-through behavior based on their configuration. /// /// /// This class relies on , which uses /// to provide access to the current . /// /// -public class ClaimsIdentitySessionIsolationKeyProvider : SessionIsolationKeyProvider +public class ClaimsIdentityAgentIsolationKeyProvider : AgentIsolationKeyProvider { private readonly IHttpContextAccessor? _httpContextAccessor; private readonly string _claimType; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// The used to retrieve the current HTTP context and user claims. /// /// The options for configuring the provider. If null, defaults are used. /// - /// is null, empty, or whitespace. + /// is null, empty, or whitespace. /// - public ClaimsIdentitySessionIsolationKeyProvider( + public ClaimsIdentityAgentIsolationKeyProvider( IHttpContextAccessor? httpContextAccessor, - ClaimsIdentitySessionIsolationKeyProviderOptions? options = null) + ClaimsIdentityAgentIsolationKeyProviderOptions? options = null) { - options ??= new ClaimsIdentitySessionIsolationKeyProviderOptions(); + options ??= new ClaimsIdentityAgentIsolationKeyProviderOptions(); this._httpContextAccessor = httpContextAccessor; this._claimType = Throw.IfNullOrWhitespace(options.ClaimType); } /// - /// Extracts the session isolation key from the current user's claims. + /// Extracts the isolation key for agent-owned resources from the current user's claims. /// /// The to monitor for cancellation requests. /// @@ -81,7 +81,7 @@ public ClaimsIdentitySessionIsolationKeyProvider( /// unauthenticated identity. The claim value is retrieved from HttpContext.User.Claims; if /// multiple claims of the specified type exist, the first match is returned. /// - public override ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + public override ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken = default) { ClaimsPrincipal? user = this._httpContextAccessor?.HttpContext?.User; if (user?.Identity?.IsAuthenticated != true) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentityAgentIsolationKeyProviderOptions.cs similarity index 84% rename from dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs rename to dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentityAgentIsolationKeyProviderOptions.cs index f929134a5ae..fefebd0aa97 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentityAgentIsolationKeyProviderOptions.cs @@ -5,12 +5,12 @@ namespace Microsoft.Agents.AI.Hosting; /// -/// Options for configuring . +/// Options for configuring . /// -public class ClaimsIdentitySessionIsolationKeyProviderOptions +public class ClaimsIdentityAgentIsolationKeyProviderOptions { /// - /// Gets or sets the claim type to extract from the user's identity for session isolation. + /// Gets or sets the claim type to extract from the user's identity to isolate agent-owned resources. /// /// /// @@ -28,8 +28,8 @@ public class ClaimsIdentitySessionIsolationKeyProviderOptions /// / ), usernames, email aliases, and other mutable or non-unique /// claims are unsafe isolation keys unless the host can prove their uniqueness /// across all callers. Two distinct principals that share the same value for a non-unique claim - /// would receive the same session-isolation key and could read or overwrite one another's - /// persisted sessions. Only override this value with a claim that is guaranteed unique and stable. + /// would receive the same agent-isolation key and could read or overwrite one another's + /// persisted data. Only override this value with a claim that is guaranteed unique and stable. /// /// /// Common alternatives include: diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs index 8196d40e812..6f27af7d1bc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs @@ -13,11 +13,11 @@ namespace Microsoft.Agents.AI.Hosting; public static class ServiceCollectionExtensions { /// - /// Registers a that uses claims from the current user's identity - /// to generate session isolation keys. + /// Registers an that uses claims from the current user's identity + /// to generate isolation keys for agent-owned resources. /// /// The to add services to. - /// Optional configuration for the claims-based session isolation key provider. + /// Optional configuration for the claims-based isolation key provider. /// The so that additional calls can be chained. /// /// @@ -31,24 +31,24 @@ public static class ServiceCollectionExtensions /// by the default JWT inbound claim mapping. Authentication schemes that do not project a unique /// identifier onto (or hosts that require a different claim /// such as Entra's oid) should override - /// ; otherwise the key may be - /// absent, which causes strict-mode session stores to fail. + /// ; otherwise the key may be + /// absent, which causes strict-mode stores to fail. /// /// /// Security warning: If you override - /// , the chosen claim must + /// , the chosen claim must /// uniquely identify the principal within the served population. Display names, usernames, email /// aliases, and other mutable or non-unique claims are unsafe isolation keys unless /// the host can prove their uniqueness across all callers, because distinct principals that share the - /// same claim value would receive the same isolation key and could access one another's sessions. + /// same claim value would receive the same isolation key and could access one another's persisted data. /// /// - public static IServiceCollection UseClaimsBasedSessionIsolation( + public static IServiceCollection UseClaimsBasedAgentIsolation( this IServiceCollection services, - ClaimsIdentitySessionIsolationKeyProviderOptions? options = null) + ClaimsIdentityAgentIsolationKeyProviderOptions? options = null) { options ??= new(); - ServiceDescriptor descriptor = new(typeof(SessionIsolationKeyProvider), CreateIsolationKeyProvider, ServiceLifetime.Singleton); + ServiceDescriptor descriptor = new(typeof(AgentIsolationKeyProvider), CreateIsolationKeyProvider, ServiceLifetime.Singleton); services.Add(descriptor); return services; @@ -57,7 +57,7 @@ object CreateIsolationKeyProvider(IServiceProvider serviceProvider) { IHttpContextAccessor contextAccessor = serviceProvider.GetRequiredService(); - return new ClaimsIdentitySessionIsolationKeyProvider(contextAccessor, options); + return new ClaimsIdentityAgentIsolationKeyProvider(contextAccessor, options); } } } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentIsolationKeyProvider.cs similarity index 50% rename from dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs rename to dotnet/src/Microsoft.Agents.AI.Hosting/AgentIsolationKeyProvider.cs index 61ea82bd343..c337ee24938 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentIsolationKeyProvider.cs @@ -6,24 +6,26 @@ namespace Microsoft.Agents.AI.Hosting; /// -/// Provides an abstract base class for resolving session isolation keys used to scope agent sessions. +/// Provides an abstract base class for resolving keys that isolate resources owned by hosted agents. /// /// /// -/// Session isolation keys enable multi-tenant or multi-user scenarios by scoping agent session storage -/// to a specific logical partition (e.g., user ID, tenant ID, or composite key). Derived classes -/// implement the key resolution logic appropriate to their hosting environment. +/// The Agent prefix identifies the hosting API domain; it does not mean that agent instances +/// themselves are isolated. The returned key scopes agent-owned resources, such as sessions and A2A +/// tasks, to a logical partition (e.g., user ID, tenant ID, or composite key). Other agent resources, +/// such as memory or retrieval data, can use the same key when they require the same isolation boundary. +/// Derived classes implement the key resolution logic appropriate to their hosting environment. /// /// /// When a key is unavailable or cannot be determined, implementations should return . -/// The consuming session store can then enforce strict behavior (throwing an exception) or fall back -/// to unscoped storage based on its configuration. +/// Consuming stores can then enforce strict behavior (throwing an exception) or fall back to unscoped +/// storage based on their configuration. /// /// -public abstract class SessionIsolationKeyProvider +public abstract class AgentIsolationKeyProvider { /// - /// Asynchronously retrieves the session isolation key for the current request or execution context. + /// Asynchronously retrieves the isolation key for agent-owned resources in the current request or execution context. /// /// The to monitor for cancellation requests. /// @@ -35,5 +37,5 @@ public abstract class SessionIsolationKeyProvider /// or environment variables). If the key cannot be determined, return to allow /// the caller to decide on strict vs. pass-through behavior. /// - public abstract ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default); + public abstract ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs index e3d1c20699b..85e3985ab8b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs @@ -27,10 +27,10 @@ namespace Microsoft.Agents.AI.Hosting; /// who knows or guesses another caller's sessionStoreId can resume /// that other caller's persisted thread. The framework provides /// as a decorator that rewrites -/// sessionStoreId 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 +/// sessionStoreId to include an isolation key resolved from an +/// (for example, the ASP.NET Core +/// ClaimsIdentityAgentIsolationKeyProvider wired up via +/// UseClaimsBasedAgentIsolation(...)). 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. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs index ed11840f5eb..a13eab90384 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs @@ -59,7 +59,7 @@ public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder buil if (withIsolation && store.GetService() is null) { - var isolationKeyProvider = sp.GetService(); + var isolationKeyProvider = sp.GetService(); // Best efforts options getting IsolationKeyScopedAgentSessionStoreOptions? options = sp.GetService(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs index 87c33a567a9..55935530f50 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs @@ -8,12 +8,12 @@ namespace Microsoft.Agents.AI.Hosting; /// /// A delegating that scopes session keys by an isolation key -/// provided by a , ensuring that sessions are isolated +/// provided by an , ensuring that sessions are isolated /// per logical partition (e.g., user, tenant, or composite key). /// public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore { - private readonly SessionIsolationKeyProvider? _keyProvider; + private readonly AgentIsolationKeyProvider? _keyProvider; private readonly bool _strict; /// @@ -21,7 +21,7 @@ public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore /// /// The underlying to delegate to. /// - /// The used to retrieve the isolation key for the current context. + /// The used to retrieve the isolation key for the current context. /// /// The options for configuring the session store. If null, defaults are used. /// @@ -29,7 +29,7 @@ public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore /// public IsolationKeyScopedAgentSessionStore( AgentSessionStore innerStore, - SessionIsolationKeyProvider? keyProvider, + AgentIsolationKeyProvider? keyProvider, IsolationKeyScopedAgentSessionStoreOptions? options = null) : base(innerStore) { @@ -51,12 +51,12 @@ public IsolationKeyScopedAgentSessionStore( private async ValueTask 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; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs index 94f00f01bb3..773ee96206e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs @@ -13,7 +13,7 @@ public class IsolationKeyScopedAgentSessionStoreOptions /// /// /// If (default), the store will throw an - /// when returns . + /// when returns . /// /// /// If , the conversation ID is passed through unmodified when the isolation key is absent, diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs index 2c17d7e2f3b..448d20f473f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs @@ -32,9 +32,9 @@ namespace Microsoft.Agents.AI.Hosting; /// 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 +/// UseClaimsBasedAgentIsolation(...) from /// Microsoft.Agents.AI.Hosting.AspNetCore or by registering a custom -/// ) so that the conversation namespace is +/// ) so that the conversation namespace is /// scoped per principal. See the trust-model remarks on /// for the full background. /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs index 21581e79f7c..24294f5c9ca 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AServerServiceCollectionExtensionsTests.cs @@ -106,7 +106,7 @@ public async Task AddA2AServer_WithCustomTaskStore_ResolvesSuccessfullyAsync() } /// - /// Verifies that when a SessionIsolationKeyProvider is registered, task operations + /// Verifies that when an AgentIsolationKeyProvider is registered, task operations /// use scoped identifiers (DI wiring test). /// [Fact] @@ -126,9 +126,9 @@ public async Task AddA2AServer_WithIsolationKeyProvider_TaskStoreReceivesScopedI .ReturnsAsync(new AgentTask { Id = TaskId, ContextId = $"{IsolationKey}::ctx-1", Status = new global::A2A.TaskStatus { State = TaskState.Completed } }); services.AddKeyedSingleton(AgentName, mockTaskStore.Object); - var mockKeyProvider = new Mock(); + var mockKeyProvider = new Mock(); mockKeyProvider - .Setup(p => p.GetSessionIsolationKeyAsync(It.IsAny())) + .Setup(p => p.GetIsolationKeyAsync(It.IsAny())) .ReturnsAsync(IsolationKey); services.AddSingleton(mockKeyProvider.Object); @@ -163,9 +163,9 @@ public async Task AddA2AServer_WithAlreadyWrappedTaskStore_DoesNotDoubleWrapAsyn .Setup(s => s.GetTaskAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new AgentTask { Id = TaskId, ContextId = $"{IsolationKey}::ctx-1", Status = new global::A2A.TaskStatus { State = TaskState.Completed } }); - var mockKeyProvider = new Mock(); + var mockKeyProvider = new Mock(); mockKeyProvider - .Setup(p => p.GetSessionIsolationKeyAsync(It.IsAny())) + .Setup(p => p.GetIsolationKeyAsync(It.IsAny())) .ReturnsAsync(IsolationKey); // Pre-wrap the task store diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/IsolationKeyScopedTaskStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/IsolationKeyScopedTaskStoreTests.cs index ed7e3c530d8..b066d66dbe9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/IsolationKeyScopedTaskStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/IsolationKeyScopedTaskStoreTests.cs @@ -325,10 +325,10 @@ public void Constructor_NullInnerStore_ThrowsArgumentNullException() Assert.Throws(() => new IsolationKeyScopedTaskStore(null!, null, strict: false)); } - private static SessionIsolationKeyProvider CreateKeyProvider(string? key) + private static AgentIsolationKeyProvider CreateKeyProvider(string? key) { - var mock = new Mock(); - mock.Setup(p => p.GetSessionIsolationKeyAsync(It.IsAny())) + var mock = new Mock(); + mock.Setup(p => p.GetIsolationKeyAsync(It.IsAny())) .ReturnsAsync(key); return mock.Object; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentIsolationKeyProviderTests.cs similarity index 52% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentIsolationKeyProviderTests.cs index 00bf2cd3738..4f8ebb0b536 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentIsolationKeyProviderTests.cs @@ -6,22 +6,22 @@ namespace Microsoft.Agents.AI.Hosting.UnitTests; /// -/// Unit tests for and its contract. +/// Unit tests for and its contract. /// -public class SessionIsolationKeyProviderTests +public class AgentIsolationKeyProviderTests { /// /// Verify that a concrete provider can return a non-null isolation key. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsNonNullKeyAsync() + public async Task GetIsolationKeyAsyncReturnsNonNullKeyAsync() { // Arrange const string ExpectedKey = "test-key"; - var provider = new TestSessionIsolationKeyProvider(ExpectedKey); + var provider = new TestAgentIsolationKeyProvider(ExpectedKey); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Equal(ExpectedKey, result); @@ -31,13 +31,13 @@ public async Task GetSessionIsolationKeyAsyncReturnsNonNullKeyAsync() /// Verify that a concrete provider can return null when no key is available. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsNullWhenNoKeyAvailableAsync() + public async Task GetIsolationKeyAsyncReturnsNullWhenNoKeyAvailableAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(null); + var provider = new TestAgentIsolationKeyProvider(null); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Null(result); @@ -47,33 +47,33 @@ public async Task GetSessionIsolationKeyAsyncReturnsNullWhenNoKeyAvailableAsync( /// Verify that cancellation token is passed through to the provider implementation. /// [Fact] - public async Task GetSessionIsolationKeyAsyncPassesCancellationTokenAsync() + public async Task GetIsolationKeyAsyncPassesCancellationTokenAsync() { // Arrange - var provider = new TestCancellableSessionIsolationKeyProvider(); + var provider = new TestCancellableAgentIsolationKeyProvider(); using var cts = new CancellationTokenSource(); cts.Cancel(); // Act & Assert await Assert.ThrowsAsync( - async () => await provider.GetSessionIsolationKeyAsync(cts.Token)); + async () => await provider.GetIsolationKeyAsync(cts.Token)); } #region Test Implementations /// - /// Test implementation of for testing purposes. + /// Test implementation of for testing purposes. /// - private sealed class TestSessionIsolationKeyProvider : SessionIsolationKeyProvider + private sealed class TestAgentIsolationKeyProvider : AgentIsolationKeyProvider { private readonly string? _key; - public TestSessionIsolationKeyProvider(string? key) + public TestAgentIsolationKeyProvider(string? key) { this._key = key; } - public override ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + public override ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken = default) { return new ValueTask(this._key); } @@ -82,9 +82,9 @@ public TestSessionIsolationKeyProvider(string? key) /// /// Test implementation that respects cancellation tokens. /// - private sealed class TestCancellableSessionIsolationKeyProvider : SessionIsolationKeyProvider + private sealed class TestCancellableAgentIsolationKeyProvider : AgentIsolationKeyProvider { - public override async ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + public override async ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken = default) { await Task.Delay(1000, cancellationToken); return "key"; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentityAgentIsolationKeyProviderTests.cs similarity index 63% rename from dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentityAgentIsolationKeyProviderTests.cs index 6434d0cdae0..f62c62e7bd4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentityAgentIsolationKeyProviderTests.cs @@ -9,9 +9,9 @@ namespace Microsoft.Agents.AI.Hosting.UnitTests; /// -/// Unit tests for . +/// Unit tests for . /// -public class ClaimsIdentitySessionIsolationKeyProviderTests +public class ClaimsIdentityAgentIsolationKeyProviderTests { private const string TestUserId = "test-user-id"; private const string CustomClaimType = "custom-claim-type"; @@ -21,9 +21,9 @@ public class ClaimsIdentitySessionIsolationKeyProviderTests private readonly Mock _httpContextAccessorMock; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ClaimsIdentitySessionIsolationKeyProviderTests() + public ClaimsIdentityAgentIsolationKeyProviderTests() { this._httpContextAccessorMock = new Mock(); } @@ -37,7 +37,7 @@ public ClaimsIdentitySessionIsolationKeyProviderTests() public void UsesDefaultOptionsWhenNull() { // Act & Assert - should not throw - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object, options: null); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object, options: null); Assert.NotNull(provider); } @@ -48,7 +48,7 @@ public void UsesDefaultOptionsWhenNull() public void Constructor_WithNullHttpContextAccessor_DoesNotThrow() { // Act & Assert - should not throw - var provider = new ClaimsIdentitySessionIsolationKeyProvider(httpContextAccessor: null); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(httpContextAccessor: null); Assert.NotNull(provider); } @@ -60,9 +60,9 @@ public void RequiresClaimType_NotNull() { // Act & Assert Assert.Throws("options.ClaimType", () => - new ClaimsIdentitySessionIsolationKeyProvider( + new ClaimsIdentityAgentIsolationKeyProvider( this._httpContextAccessorMock.Object, - new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = null! })); + new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = null! })); } /// @@ -73,9 +73,9 @@ public void RequiresClaimType_NotEmpty() { // Act & Assert Assert.Throws("options.ClaimType", () => - new ClaimsIdentitySessionIsolationKeyProvider( + new ClaimsIdentityAgentIsolationKeyProvider( this._httpContextAccessorMock.Object, - new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = string.Empty })); + new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = string.Empty })); } /// @@ -86,27 +86,27 @@ public void RequiresClaimType_NotWhitespace() { // Act & Assert Assert.Throws("options.ClaimType", () => - new ClaimsIdentitySessionIsolationKeyProvider( + new ClaimsIdentityAgentIsolationKeyProvider( this._httpContextAccessorMock.Object, - new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = " " })); + new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = " " })); } #endregion - #region GetSessionIsolationKeyAsync Tests + #region GetIsolationKeyAsync Tests /// - /// Verify that GetSessionIsolationKeyAsync extracts the claim value from the default claim type. + /// Verify that GetIsolationKeyAsync extracts the claim value from the default claim type. /// [Fact] - public async Task GetSessionIsolationKeyAsyncExtractsDefaultClaimTypeAsync() + public async Task GetIsolationKeyAsyncExtractsDefaultClaimTypeAsync() { // Arrange this.SetupHttpContextWithClaim(ClaimTypes.NameIdentifier, TestUserId); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Equal(TestUserId, result); @@ -114,54 +114,54 @@ public async Task GetSessionIsolationKeyAsyncExtractsDefaultClaimTypeAsync() /// /// Verify that the default claim type is the stable, unique NameIdentifier claim rather than the - /// non-unique display name claim. This guards against the session-isolation collision described in + /// non-unique display name claim. This guards against the resource-isolation collision described in /// the security report where two principals sharing the same name claim received the same key. /// [Fact] - public async Task GetSessionIsolationKeyAsyncIgnoresNameClaimByDefaultAsync() + public async Task GetIsolationKeyAsyncIgnoresNameClaimByDefaultAsync() { // Arrange - only a display-name claim is present; the default provider must not use it. this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Null(result); } /// - /// Verify that GetSessionIsolationKeyAsync uses custom claim type when specified. + /// Verify that GetIsolationKeyAsync uses custom claim type when specified. /// [Fact] - public async Task GetSessionIsolationKeyAsyncUsesCustomClaimTypeAsync() + public async Task GetIsolationKeyAsyncUsesCustomClaimTypeAsync() { // Arrange this.SetupHttpContextWithClaim(CustomClaimType, CustomClaimValue); - var provider = new ClaimsIdentitySessionIsolationKeyProvider( + var provider = new ClaimsIdentityAgentIsolationKeyProvider( this._httpContextAccessorMock.Object, - new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = CustomClaimType }); + new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = CustomClaimType }); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Equal(CustomClaimValue, result); } /// - /// Verify that GetSessionIsolationKeyAsync returns null when the specified claim is missing. + /// Verify that GetIsolationKeyAsync returns null when the specified claim is missing. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsNullWhenClaimMissingAsync() + public async Task GetIsolationKeyAsyncReturnsNullWhenClaimMissingAsync() { // Arrange this.SetupHttpContextWithClaim("other-claim", "value"); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Null(result); @@ -171,14 +171,14 @@ public async Task GetSessionIsolationKeyAsyncReturnsNullWhenClaimMissingAsync() /// Verify behavior when HttpContextAccessor returns null HttpContext. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextNullAsync() + public async Task GetIsolationKeyAsyncReturnsNullWhenHttpContextNullAsync() { // Arrange this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns((HttpContext?)null); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Null(result); @@ -188,23 +188,23 @@ public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextNullAsync /// Verify behavior when HttpContextAccessor itself is null. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextAccessorNullAsync() + public async Task GetIsolationKeyAsyncReturnsNullWhenHttpContextAccessorNullAsync() { // Arrange - var provider = new ClaimsIdentitySessionIsolationKeyProvider(httpContextAccessor: null); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(httpContextAccessor: null); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Null(result); } /// - /// Verify that GetSessionIsolationKeyAsync returns the first matching claim when multiple exist. + /// Verify that GetIsolationKeyAsync returns the first matching claim when multiple exist. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsFirstMatchingClaimAsync() + public async Task GetIsolationKeyAsyncReturnsFirstMatchingClaimAsync() { // Arrange const string FirstValue = "first-value"; @@ -223,39 +223,39 @@ public async Task GetSessionIsolationKeyAsyncReturnsFirstMatchingClaimAsync() }; this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Equal(FirstValue, result); } /// - /// Verify that GetSessionIsolationKeyAsync handles empty claim values. + /// Verify that GetIsolationKeyAsync handles empty claim values. /// [Fact] - public async Task GetSessionIsolationKeyAsyncHandlesEmptyClaimValueAsync() + public async Task GetIsolationKeyAsyncHandlesEmptyClaimValueAsync() { // Arrange this.SetupHttpContextWithClaim(ClaimTypes.NameIdentifier, string.Empty); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.Equal(string.Empty, result); } /// - /// Regression test for the session-isolation collision security report: two distinct authenticated + /// Regression test for the resource-isolation collision security report: two distinct authenticated /// principals that share the same display-name claim but have different stable identifiers and tenants /// must produce distinct isolation keys under the default options. /// [Fact] - public async Task GetSessionIsolationKeyAsyncDistinctForPrincipalsSharingNameClaimAsync() + public async Task GetIsolationKeyAsyncDistinctForPrincipalsSharingNameClaimAsync() { // Arrange - both principals share the same name claim but differ by NameIdentifier and tenant. const string CommonName = "John Doe"; @@ -270,14 +270,14 @@ public async Task GetSessionIsolationKeyAsyncDistinctForPrincipalsSharingNameCla new Claim(ClaimTypes.NameIdentifier, "oid-user-b"), new Claim("http://schemas.microsoft.com/identity/claims/tenantid", "tenant-b")); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(new DefaultHttpContext { User = principalA }); - string? principalAKey = await provider.GetSessionIsolationKeyAsync(); + string? principalAKey = await provider.GetIsolationKeyAsync(); this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(new DefaultHttpContext { User = principalB }); - string? principalBKey = await provider.GetSessionIsolationKeyAsync(); + string? principalBKey = await provider.GetIsolationKeyAsync(); // Assert Assert.Equal("oid-user-a", principalAKey); @@ -286,12 +286,12 @@ public async Task GetSessionIsolationKeyAsyncDistinctForPrincipalsSharingNameCla } /// - /// Verify that GetSessionIsolationKeyAsync returns null when the request's user is not authenticated, + /// Verify that GetIsolationKeyAsync returns null when the request's user is not authenticated, /// even if a claim of the configured type is present. The provider must not derive an isolation key /// from claims on an unauthenticated identity. /// [Fact] - public async Task GetSessionIsolationKeyAsyncReturnsNullWhenUserNotAuthenticatedAsync() + public async Task GetIsolationKeyAsyncReturnsNullWhenUserNotAuthenticatedAsync() { // Arrange - identity has the claim but no authentication type, so IsAuthenticated is false. var claims = new[] { new Claim(ClaimTypes.NameIdentifier, TestUserId) }; @@ -299,10 +299,10 @@ public async Task GetSessionIsolationKeyAsyncReturnsNullWhenUserNotAuthenticated var principal = new ClaimsPrincipal(unauthenticatedIdentity); var httpContext = new DefaultHttpContext { User = principal }; this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext); - var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + var provider = new ClaimsIdentityAgentIsolationKeyProvider(this._httpContextAccessorMock.Object); // Act - string? result = await provider.GetSessionIsolationKeyAsync(); + string? result = await provider.GetIsolationKeyAsync(); // Assert Assert.False(unauthenticatedIdentity.IsAuthenticated); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs index aaaffe797b4..2521d06a113 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs @@ -46,7 +46,7 @@ public IsolationKeyScopedAgentSessionStoreTests() public void RequiresInnerStore() { // Arrange - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); // Act & Assert Assert.Throws("innerStore", () => @@ -60,7 +60,7 @@ public void RequiresInnerStore() public void UsesDefaultOptionsWhenNull() { // Arrange - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); // Act & Assert - should not throw var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider, options: null); @@ -78,7 +78,7 @@ public void UsesDefaultOptionsWhenNull() public async Task GetSessionAsyncScopesConversationIdWithKeyAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); // Act @@ -100,7 +100,7 @@ public async Task GetSessionAsyncScopesConversationIdWithKeyAsync() public async Task GetSessionAsyncThrowsWhenKeyNullInStrictModeAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(null); + var provider = new TestAgentIsolationKeyProvider(null); var store = new IsolationKeyScopedAgentSessionStore( this._innerStoreMock.Object, provider, @@ -110,7 +110,7 @@ public async Task GetSessionAsyncThrowsWhenKeyNullInStrictModeAsync() var exception = await Assert.ThrowsAsync( async () => await store.GetSessionAsync(this._agentMock.Object, TestConversationId)); - Assert.Contains("Session isolation key is required", exception.Message); + Assert.Contains("Agent isolation key is required", exception.Message); } /// @@ -120,7 +120,7 @@ public async Task GetSessionAsyncThrowsWhenKeyNullInStrictModeAsync() public async Task GetSessionAsyncDoesNotThrowWhenKeyNullInNonStrictModeAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(null); + var provider = new TestAgentIsolationKeyProvider(null); var store = new IsolationKeyScopedAgentSessionStore( this._innerStoreMock.Object, provider, @@ -145,7 +145,7 @@ public async Task GetSessionAsyncDoesNotThrowWhenKeyNullInNonStrictModeAsync() public async Task GetSessionAsyncReturnsSessionFromInnerStoreAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); // Act @@ -166,7 +166,7 @@ public async Task GetSessionAsyncReturnsSessionFromInnerStoreAsync() public async Task SaveSessionAsyncScopesConversationIdWithKeyAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); var sessionToSave = new TestAgentSession(); @@ -190,7 +190,7 @@ public async Task SaveSessionAsyncScopesConversationIdWithKeyAsync() public async Task SaveSessionAsyncThrowsWhenKeyNullInStrictModeAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(null); + var provider = new TestAgentIsolationKeyProvider(null); var store = new IsolationKeyScopedAgentSessionStore( this._innerStoreMock.Object, provider, @@ -201,7 +201,7 @@ public async Task SaveSessionAsyncThrowsWhenKeyNullInStrictModeAsync() var exception = await Assert.ThrowsAsync( async () => await store.SaveSessionAsync(this._agentMock.Object, TestConversationId, sessionToSave)); - Assert.Contains("Session isolation key is required", exception.Message); + Assert.Contains("Agent isolation key is required", exception.Message); } /// @@ -211,7 +211,7 @@ public async Task SaveSessionAsyncThrowsWhenKeyNullInStrictModeAsync() public async Task SaveSessionAsyncDoesNotThrowWhenKeyNullInNonStrictModeAsync() { // Arrange - var provider = new TestSessionIsolationKeyProvider(null); + var provider = new TestAgentIsolationKeyProvider(null); var store = new IsolationKeyScopedAgentSessionStore( this._innerStoreMock.Object, provider, @@ -243,7 +243,7 @@ public async Task EscapesColonsInIsolationKeyAsync() { // Arrange const string KeyWithColon = "key:with:colons"; - var provider = new TestSessionIsolationKeyProvider(KeyWithColon); + var provider = new TestAgentIsolationKeyProvider(KeyWithColon); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); // Act @@ -266,7 +266,7 @@ public async Task EscapesBackslashesInIsolationKeyAsync() { // Arrange const string KeyWithBackslash = @"domain\key"; - var provider = new TestSessionIsolationKeyProvider(KeyWithBackslash); + var provider = new TestAgentIsolationKeyProvider(KeyWithBackslash); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); // Act @@ -289,7 +289,7 @@ public async Task EscapesBothBackslashesAndColonsInIsolationKeyAsync() { // Arrange const string KeyWithBoth = @"domain\key:role"; - var provider = new TestSessionIsolationKeyProvider(KeyWithBoth); + var provider = new TestAgentIsolationKeyProvider(KeyWithBoth); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); // Act @@ -336,12 +336,12 @@ public async Task DifferentKeysResultInDifferentScopedConversationIdsAsync() .ReturnsAsync(this._testSession); // Act - Key 1 - var provider1 = new TestSessionIsolationKeyProvider(Key1); + var provider1 = new TestAgentIsolationKeyProvider(Key1); var store1 = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider1); await store1.GetSessionAsync(this._agentMock.Object, TestConversationId); // Act - Key 2 - var provider2 = new TestSessionIsolationKeyProvider(Key2); + var provider2 = new TestAgentIsolationKeyProvider(Key2); var store2 = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider2); await store2.GetSessionAsync(this._agentMock.Object, TestConversationId); @@ -362,7 +362,7 @@ public async Task DifferentKeysResultInDifferentScopedConversationIdsAsync() public void GetServiceReturnsIsolationKeyScopedAgentSessionStore() { // Arrange - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); // Act @@ -380,7 +380,7 @@ public void GetServiceChainsToInnerStore() { // Arrange var concreteInnerStore = new ConcreteAgentSessionStore(); - var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var provider = new TestAgentIsolationKeyProvider(TestIsolationKey); var store = new IsolationKeyScopedAgentSessionStore(concreteInnerStore, provider); // Act @@ -395,18 +395,18 @@ public void GetServiceChainsToInnerStore() #region Helper Classes /// - /// Test implementation of for testing purposes. + /// Test implementation of for testing purposes. /// - private sealed class TestSessionIsolationKeyProvider : SessionIsolationKeyProvider + private sealed class TestAgentIsolationKeyProvider : AgentIsolationKeyProvider { private readonly string? _key; - public TestSessionIsolationKeyProvider(string? key) + public TestAgentIsolationKeyProvider(string? key) { this._key = key; } - public override ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + public override ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken = default) { return new ValueTask(this._key); }