.NET: [BREAKING] Rename to AgentIsolationKeyProvider (#7567)
* Update store isolation documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292 * Rename store isolation key provider Rename the shared session isolation abstraction to reflect its use for both session and task stores. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292 * Rename to AgentIsolationKeyProvider per review feedback Drops the `Store` qualifier and keeps an `Agent` prefix so the type is not confused with generic isolation-key abstractions from other libraries, while leaving room for future non-store isolation (memory, retrieval). - StoreIsolationKeyProvider -> AgentIsolationKeyProvider - ClaimsIdentityStoreIsolationKeyProvider(+Options) -> ClaimsIdentityAgentIsolationKeyProvider(+Options) - GetStoreIsolationKeyAsync -> GetIsolationKeyAsync - UseClaimsBasedStoreIsolation -> UseClaimsBasedAgentIsolation XML docs now state that the `Agent` prefix identifies the hosting API domain and does not mean agent instances are isolated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292 * Update hosting spec for AgentIsolationKeyProvider rename Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ builder.Services.AddHttpClient().AddLogging();
|
||||
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();
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
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();
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ builder.Services.AddHttpClient().AddLogging();
|
||||
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();
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
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();
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ builder.Services.AddAGUIServer();
|
||||
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();
|
||||
|
||||
|
||||
@@ -101,10 +101,10 @@ else
|
||||
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.:
|
||||
|
||||
@@ -20,9 +20,9 @@ builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.T
|
||||
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();
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ var agent = new AzureOpenAIClient(
|
||||
]);
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -13,9 +13,9 @@ builder.Services.AddHttpClient().AddLogging();
|
||||
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();
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ builder.AddDevUI();
|
||||
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 @@ builder.Services.AddKeyedSingleton<AIAgent>("my-di-matchingname-agent", (sp, nam
|
||||
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();
|
||||
|
||||
|
||||
+11
-11
@@ -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
|
||||
@@ -68,8 +68,8 @@ public static class A2AServerServiceCollectionExtensions
|
||||
/// 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)
|
||||
{
|
||||
@@ -94,8 +94,8 @@ public static class A2AServerServiceCollectionExtensions
|
||||
/// 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)
|
||||
{
|
||||
@@ -119,8 +119,8 @@ public static class A2AServerServiceCollectionExtensions
|
||||
/// 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)
|
||||
{
|
||||
@@ -157,8 +157,8 @@ public static class A2AServerServiceCollectionExtensions
|
||||
/// 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)
|
||||
{
|
||||
@@ -180,7 +180,7 @@ public static class A2AServerServiceCollectionExtensions
|
||||
|
||||
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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,7 +36,7 @@ public sealed class IsolationKeyScopedTaskStore : ITaskStore
|
||||
/// </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
|
||||
@@ -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);
|
||||
@@ -130,12 +130,12 @@ public sealed class IsolationKeyScopedTaskStore : ITaskStore
|
||||
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;
|
||||
|
||||
+3
-3
@@ -91,9 +91,9 @@ public static class AGUIEndpointRouteBuilderExtensions
|
||||
/// 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
|
||||
@@ -113,7 +113,7 @@ public static class AGUIEndpointRouteBuilderExtensions
|
||||
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();
|
||||
|
||||
+17
-17
@@ -11,63 +11,63 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="SessionIsolationKeyProvider"/> that extracts the session isolation key from a claim
|
||||
/// in the current user's identity, as provided by ASP.NET Core's <see cref="IHttpContextAccessor"/>.
|
||||
/// An <see cref="AgentIsolationKeyProvider"/> 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 <see cref="IHttpContextAccessor"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 <see cref="HttpContext"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security warning:</strong> The configured <see cref="ClaimsIdentitySessionIsolationKeyProviderOptions.ClaimType"/>
|
||||
/// <strong>Security warning:</strong> The configured <see cref="ClaimsIdentityAgentIsolationKeyProviderOptions.ClaimType"/>
|
||||
/// must uniquely identify the principal within the served population. Display names, usernames, email
|
||||
/// aliases, and other mutable or non-unique claims are <strong>unsafe</strong> 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 <see cref="ClaimTypes.NameIdentifier"/>, a stable unique subject identifier
|
||||
/// that is typically populated from the OpenID Connect <c>sub</c> claim via the default JWT inbound claim
|
||||
/// mapping (note that this differs from Entra's object identifier <c>oid</c> claim; override
|
||||
/// <see cref="ClaimsIdentitySessionIsolationKeyProviderOptions.ClaimType"/> if you need <c>oid</c> or your
|
||||
/// <see cref="ClaimsIdentityAgentIsolationKeyProviderOptions.ClaimType"/> if you need <c>oid</c> or your
|
||||
/// provider maps a different claim).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the <see cref="HttpContext"/> is unavailable, the user is not authenticated, or the specified claim
|
||||
/// is missing, the provider returns <see langword="null"/>. The consuming <see cref="IsolationKeyScopedAgentSessionStore"/>
|
||||
/// will then enforce strict or pass-through behavior based on its configuration.
|
||||
/// is missing, the provider returns <see langword="null"/>. Consuming stores then enforce strict or
|
||||
/// pass-through behavior based on their configuration.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This class relies on <see cref="IHttpContextAccessor"/>, which uses <see cref="AsyncLocal{T}"/>
|
||||
/// to provide access to the current <see cref="HttpContext"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class ClaimsIdentitySessionIsolationKeyProvider : SessionIsolationKeyProvider
|
||||
public class ClaimsIdentityAgentIsolationKeyProvider : AgentIsolationKeyProvider
|
||||
{
|
||||
private readonly IHttpContextAccessor? _httpContextAccessor;
|
||||
private readonly string _claimType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClaimsIdentitySessionIsolationKeyProvider"/> class.
|
||||
/// Initializes a new instance of the <see cref="ClaimsIdentityAgentIsolationKeyProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">
|
||||
/// The <see cref="IHttpContextAccessor"/> used to retrieve the current HTTP context and user claims.
|
||||
/// </param>
|
||||
/// <param name="options">The options for configuring the provider. If null, defaults are used.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <see cref="ClaimsIdentitySessionIsolationKeyProviderOptions.ClaimType"/> is null, empty, or whitespace.
|
||||
/// <see cref="ClaimsIdentityAgentIsolationKeyProviderOptions.ClaimType"/> is null, empty, or whitespace.
|
||||
/// </exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>
|
||||
@@ -81,7 +81,7 @@ public class ClaimsIdentitySessionIsolationKeyProvider : SessionIsolationKeyProv
|
||||
/// unauthenticated identity. The claim value is retrieved from <c>HttpContext.User.Claims</c>; if
|
||||
/// multiple claims of the specified type exist, the first match is returned.
|
||||
/// </remarks>
|
||||
public override ValueTask<string?> GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
public override ValueTask<string?> GetIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ClaimsPrincipal? user = this._httpContextAccessor?.HttpContext?.User;
|
||||
if (user?.Identity?.IsAuthenticated != true)
|
||||
+5
-5
@@ -5,12 +5,12 @@ using System.Security.Claims;
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring <see cref="ClaimsIdentitySessionIsolationKeyProvider"/>.
|
||||
/// Options for configuring <see cref="ClaimsIdentityAgentIsolationKeyProvider"/>.
|
||||
/// </summary>
|
||||
public class ClaimsIdentitySessionIsolationKeyProviderOptions
|
||||
public class ClaimsIdentityAgentIsolationKeyProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -28,8 +28,8 @@ public class ClaimsIdentitySessionIsolationKeyProviderOptions
|
||||
/// / <see cref="ClaimTypes.Name"/>), usernames, email aliases, and other mutable or non-unique
|
||||
/// claims are <strong>unsafe</strong> 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Common alternatives include:
|
||||
@@ -13,11 +13,11 @@ namespace Microsoft.Agents.AI.Hosting;
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a <see cref="SessionIsolationKeyProvider"/> that uses claims from the current user's identity
|
||||
/// to generate session isolation keys.
|
||||
/// Registers an <see cref="AgentIsolationKeyProvider"/> that uses claims from the current user's identity
|
||||
/// to generate isolation keys for agent-owned resources.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
|
||||
/// <param name="options"> Optional configuration for the claims-based session isolation key provider.</param>
|
||||
/// <param name="options">Optional configuration for the claims-based isolation key provider.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -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 <see cref="ClaimTypes.NameIdentifier"/> (or hosts that require a different claim
|
||||
/// such as Entra's <c>oid</c>) should override
|
||||
/// <see cref="ClaimsIdentitySessionIsolationKeyProviderOptions.ClaimType"/>; otherwise the key may be
|
||||
/// absent, which causes strict-mode session stores to fail.
|
||||
/// <see cref="ClaimsIdentityAgentIsolationKeyProviderOptions.ClaimType"/>; otherwise the key may be
|
||||
/// absent, which causes strict-mode stores to fail.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security warning:</strong> If you override
|
||||
/// <see cref="ClaimsIdentitySessionIsolationKeyProviderOptions.ClaimType"/>, the chosen claim must
|
||||
/// <see cref="ClaimsIdentityAgentIsolationKeyProviderOptions.ClaimType"/>, 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 <strong>unsafe</strong> 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
IHttpContextAccessor contextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
|
||||
|
||||
return new ClaimsIdentitySessionIsolationKeyProvider(contextAccessor, options);
|
||||
return new ClaimsIdentityAgentIsolationKeyProvider(contextAccessor, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -6,24 +6,26 @@ using System.Threading.Tasks;
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 <c>Agent</c> 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When a key is unavailable or cannot be determined, implementations should return <see langword="null"/>.
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class SessionIsolationKeyProvider
|
||||
public abstract class AgentIsolationKeyProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>
|
||||
@@ -35,5 +37,5 @@ public abstract class SessionIsolationKeyProvider
|
||||
/// or environment variables). If the key cannot be determined, return <see langword="null"/> to allow
|
||||
/// the caller to decide on strict vs. pass-through behavior.
|
||||
/// </remarks>
|
||||
public abstract ValueTask<string?> GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default);
|
||||
public abstract ValueTask<string?> GetIsolationKeyAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -27,10 +27,10 @@ namespace Microsoft.Agents.AI.Hosting;
|
||||
/// who knows or guesses another caller's <c>sessionStoreId</c> can resume
|
||||
/// that other caller's persisted thread. The framework provides
|
||||
/// <see cref="IsolationKeyScopedAgentSessionStore"/> as a decorator that rewrites
|
||||
/// <c>sessionStoreId</c> to include an isolation key resolved from a
|
||||
/// <see cref="SessionIsolationKeyProvider"/> (for example, the ASP.NET Core
|
||||
/// <c>ClaimsIdentitySessionIsolationKeyProvider</c> wired up via
|
||||
/// <c>UseClaimsBasedSessionIsolation(...)</c>). When no provider is registered, the
|
||||
/// <c>sessionStoreId</c> to include an isolation key resolved from an
|
||||
/// <see cref="AgentIsolationKeyProvider"/> (for example, the ASP.NET Core
|
||||
/// <c>ClaimsIdentityAgentIsolationKeyProvider</c> wired up via
|
||||
/// <c>UseClaimsBasedAgentIsolation(...)</c>). 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.
|
||||
/// </para>
|
||||
|
||||
@@ -59,7 +59,7 @@ public static class HostedAgentBuilderExtensions
|
||||
|
||||
if (withIsolation && store.GetService<IsolationKeyScopedAgentSessionStore>() is null)
|
||||
{
|
||||
var isolationKeyProvider = sp.GetService<SessionIsolationKeyProvider>();
|
||||
var isolationKeyProvider = sp.GetService<AgentIsolationKeyProvider>();
|
||||
|
||||
// Best efforts options getting
|
||||
IsolationKeyScopedAgentSessionStoreOptions? options = sp.GetService<IsolationKeyScopedAgentSessionStoreOptions>();
|
||||
|
||||
@@ -8,12 +8,12 @@ namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating <see cref="AgentSessionStore"/> that scopes session keys by an isolation key
|
||||
/// provided by a <see cref="SessionIsolationKeyProvider"/>, ensuring that sessions are isolated
|
||||
/// provided by an <see cref="AgentIsolationKeyProvider"/>, ensuring that sessions are isolated
|
||||
/// per logical partition (e.g., user, tenant, or composite key).
|
||||
/// </summary>
|
||||
public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore
|
||||
{
|
||||
private readonly SessionIsolationKeyProvider? _keyProvider;
|
||||
private readonly AgentIsolationKeyProvider? _keyProvider;
|
||||
private readonly bool _strict;
|
||||
|
||||
/// <summary>
|
||||
@@ -21,7 +21,7 @@ public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore
|
||||
/// </summary>
|
||||
/// <param name="innerStore">The underlying <see cref="AgentSessionStore"/> 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="options">The options for configuring the session store. If null, defaults are used.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
@@ -29,7 +29,7 @@ public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore
|
||||
/// </exception>
|
||||
public IsolationKeyScopedAgentSessionStore(
|
||||
AgentSessionStore innerStore,
|
||||
SessionIsolationKeyProvider? keyProvider,
|
||||
AgentIsolationKeyProvider? keyProvider,
|
||||
IsolationKeyScopedAgentSessionStoreOptions? options = null)
|
||||
: base(innerStore)
|
||||
{
|
||||
@@ -51,12 +51,12 @@ public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore
|
||||
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;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ public class IsolationKeyScopedAgentSessionStoreOptions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If <see langword="true"/> (default), the store will throw an <see cref="System.InvalidOperationException"/>
|
||||
/// when <see cref="SessionIsolationKeyProvider.GetSessionIsolationKeyAsync"/> returns <see langword="null"/>.
|
||||
/// when <see cref="AgentIsolationKeyProvider.GetIsolationKeyAsync"/> returns <see langword="null"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If <see langword="false"/>, the conversation ID is passed through unmodified when the isolation key is absent,
|
||||
|
||||
@@ -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
|
||||
/// <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"/>) so that the conversation namespace is
|
||||
/// <see cref="AgentIsolationKeyProvider"/>) so that the conversation namespace is
|
||||
/// scoped per principal. See the trust-model remarks on
|
||||
/// <see cref="AgentSessionStore"/> for the full background.
|
||||
/// </para>
|
||||
|
||||
+5
-5
@@ -106,7 +106,7 @@ public sealed class A2AServerServiceCollectionExtensionsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when a SessionIsolationKeyProvider is registered, task operations
|
||||
/// Verifies that when an AgentIsolationKeyProvider is registered, task operations
|
||||
/// use scoped identifiers (DI wiring test).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
@@ -126,9 +126,9 @@ public sealed class A2AServerServiceCollectionExtensionsTests
|
||||
.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<SessionIsolationKeyProvider>();
|
||||
var mockKeyProvider = new Mock<AgentIsolationKeyProvider>();
|
||||
mockKeyProvider
|
||||
.Setup(p => p.GetSessionIsolationKeyAsync(It.IsAny<CancellationToken>()))
|
||||
.Setup(p => p.GetIsolationKeyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(IsolationKey);
|
||||
services.AddSingleton(mockKeyProvider.Object);
|
||||
|
||||
@@ -163,9 +163,9 @@ public sealed class A2AServerServiceCollectionExtensionsTests
|
||||
.Setup(s => s.GetTaskAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AgentTask { Id = TaskId, ContextId = $"{IsolationKey}::ctx-1", Status = new global::A2A.TaskStatus { State = TaskState.Completed } });
|
||||
|
||||
var mockKeyProvider = new Mock<SessionIsolationKeyProvider>();
|
||||
var mockKeyProvider = new Mock<AgentIsolationKeyProvider>();
|
||||
mockKeyProvider
|
||||
.Setup(p => p.GetSessionIsolationKeyAsync(It.IsAny<CancellationToken>()))
|
||||
.Setup(p => p.GetIsolationKeyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(IsolationKey);
|
||||
|
||||
// Pre-wrap the task store
|
||||
|
||||
+3
-3
@@ -325,10 +325,10 @@ public sealed class IsolationKeyScopedTaskStoreTests
|
||||
Assert.Throws<ArgumentNullException>(() => new IsolationKeyScopedTaskStore(null!, null, strict: false));
|
||||
}
|
||||
|
||||
private static SessionIsolationKeyProvider CreateKeyProvider(string? key)
|
||||
private static AgentIsolationKeyProvider CreateKeyProvider(string? key)
|
||||
{
|
||||
var mock = new Mock<SessionIsolationKeyProvider>();
|
||||
mock.Setup(p => p.GetSessionIsolationKeyAsync(It.IsAny<CancellationToken>()))
|
||||
var mock = new Mock<AgentIsolationKeyProvider>();
|
||||
mock.Setup(p => p.GetIsolationKeyAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(key);
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
+17
-17
@@ -6,22 +6,22 @@ using System.Threading.Tasks;
|
||||
namespace Microsoft.Agents.AI.Hosting.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="SessionIsolationKeyProvider"/> and its contract.
|
||||
/// Unit tests for <see cref="AgentIsolationKeyProvider"/> and its contract.
|
||||
/// </summary>
|
||||
public class SessionIsolationKeyProviderTests
|
||||
public class AgentIsolationKeyProviderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify that a concrete provider can return a non-null isolation key.
|
||||
/// </summary>
|
||||
[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 class SessionIsolationKeyProviderTests
|
||||
/// Verify that a concrete provider can return null when no key is available.
|
||||
/// </summary>
|
||||
[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 class SessionIsolationKeyProviderTests
|
||||
/// Verify that cancellation token is passed through to the provider implementation.
|
||||
/// </summary>
|
||||
[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<TaskCanceledException>(
|
||||
async () => await provider.GetSessionIsolationKeyAsync(cts.Token));
|
||||
async () => await provider.GetIsolationKeyAsync(cts.Token));
|
||||
}
|
||||
|
||||
#region Test Implementations
|
||||
|
||||
/// <summary>
|
||||
/// Test implementation of <see cref="SessionIsolationKeyProvider"/> for testing purposes.
|
||||
/// Test implementation of <see cref="AgentIsolationKeyProvider"/> for testing purposes.
|
||||
/// </summary>
|
||||
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<string?> GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
public override ValueTask<string?> GetIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<string?>(this._key);
|
||||
}
|
||||
@@ -82,9 +82,9 @@ public class SessionIsolationKeyProviderTests
|
||||
/// <summary>
|
||||
/// Test implementation that respects cancellation tokens.
|
||||
/// </summary>
|
||||
private sealed class TestCancellableSessionIsolationKeyProvider : SessionIsolationKeyProvider
|
||||
private sealed class TestCancellableAgentIsolationKeyProvider : AgentIsolationKeyProvider
|
||||
{
|
||||
public override async ValueTask<string?> GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
public override async ValueTask<string?> GetIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
return "key";
|
||||
+53
-53
@@ -9,9 +9,9 @@ using Moq;
|
||||
namespace Microsoft.Agents.AI.Hosting.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ClaimsIdentitySessionIsolationKeyProvider"/>.
|
||||
/// Unit tests for <see cref="ClaimsIdentityAgentIsolationKeyProvider"/>.
|
||||
/// </summary>
|
||||
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<IHttpContextAccessor> _httpContextAccessorMock;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClaimsIdentitySessionIsolationKeyProviderTests"/> class.
|
||||
/// Initializes a new instance of the <see cref="ClaimsIdentityAgentIsolationKeyProviderTests"/> class.
|
||||
/// </summary>
|
||||
public ClaimsIdentitySessionIsolationKeyProviderTests()
|
||||
public ClaimsIdentityAgentIsolationKeyProviderTests()
|
||||
{
|
||||
this._httpContextAccessorMock = new Mock<IHttpContextAccessor>();
|
||||
}
|
||||
@@ -37,7 +37,7 @@ public class 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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>("options.ClaimType", () =>
|
||||
new ClaimsIdentitySessionIsolationKeyProvider(
|
||||
new ClaimsIdentityAgentIsolationKeyProvider(
|
||||
this._httpContextAccessorMock.Object,
|
||||
new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = null! }));
|
||||
new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = null! }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,9 +73,9 @@ public class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>("options.ClaimType", () =>
|
||||
new ClaimsIdentitySessionIsolationKeyProvider(
|
||||
new ClaimsIdentityAgentIsolationKeyProvider(
|
||||
this._httpContextAccessorMock.Object,
|
||||
new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = string.Empty }));
|
||||
new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = string.Empty }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,27 +86,27 @@ public class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>("options.ClaimType", () =>
|
||||
new ClaimsIdentitySessionIsolationKeyProvider(
|
||||
new ClaimsIdentityAgentIsolationKeyProvider(
|
||||
this._httpContextAccessorMock.Object,
|
||||
new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = " " }));
|
||||
new ClaimsIdentityAgentIsolationKeyProviderOptions { ClaimType = " " }));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSessionIsolationKeyAsync Tests
|
||||
#region GetIsolationKeyAsync Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetSessionIsolationKeyAsync extracts the claim value from the default claim type.
|
||||
/// Verify that GetIsolationKeyAsync extracts the claim value from the default claim type.
|
||||
/// </summary>
|
||||
[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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetSessionIsolationKeyAsync uses custom claim type when specified.
|
||||
/// Verify that GetIsolationKeyAsync uses custom claim type when specified.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetSessionIsolationKeyAsync returns null when the specified claim is missing.
|
||||
/// Verify that GetIsolationKeyAsync returns null when the specified claim is missing.
|
||||
/// </summary>
|
||||
[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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
/// Verify behavior when HttpContextAccessor returns null HttpContext.
|
||||
/// </summary>
|
||||
[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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
/// Verify behavior when HttpContextAccessor itself is null.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetSessionIsolationKeyAsync returns the first matching claim when multiple exist.
|
||||
/// Verify that GetIsolationKeyAsync returns the first matching claim when multiple exist.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetSessionIsolationKeyAsyncReturnsFirstMatchingClaimAsync()
|
||||
public async Task GetIsolationKeyAsyncReturnsFirstMatchingClaimAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string FirstValue = "first-value";
|
||||
@@ -223,39 +223,39 @@ public class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that GetSessionIsolationKeyAsync handles empty claim values.
|
||||
/// Verify that GetIsolationKeyAsync handles empty claim values.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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 class ClaimsIdentitySessionIsolationKeyProviderTests
|
||||
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);
|
||||
+22
-22
@@ -46,7 +46,7 @@ public class IsolationKeyScopedAgentSessionStoreTests
|
||||
public void RequiresInnerStore()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new TestSessionIsolationKeyProvider(TestIsolationKey);
|
||||
var provider = new TestAgentIsolationKeyProvider(TestIsolationKey);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>("innerStore", () =>
|
||||
@@ -60,7 +60,7 @@ public class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,7 +120,7 @@ public class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -211,7 +211,7 @@ public class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
{
|
||||
// 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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
{
|
||||
// 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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
{
|
||||
// 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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
.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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
{
|
||||
// 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 class IsolationKeyScopedAgentSessionStoreTests
|
||||
#region Helper Classes
|
||||
|
||||
/// <summary>
|
||||
/// Test implementation of <see cref="SessionIsolationKeyProvider"/> for testing purposes.
|
||||
/// Test implementation of <see cref="AgentIsolationKeyProvider"/> for testing purposes.
|
||||
/// </summary>
|
||||
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<string?> GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
public override ValueTask<string?> GetIsolationKeyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<string?>(this._key);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user