scope MCP threadId to the current agent

This commit is contained in:
Shyju Krishnankutty
2026-06-15 14:55:07 -07:00
Unverified
parent 40a2dd5cd0
commit f4bf0819c2
4 changed files with 111 additions and 2 deletions
@@ -53,4 +53,16 @@ public sealed class AgentSessionIdTests
AgentSessionId sessionId = entityId;
});
}
// Ensures the 2-arg constructor treats the key as opaque and never re-interprets
// it as a serialized session id, so the resulting Name always comes from the first
// argument regardless of the key's shape.
[Fact]
public void ConstructorTreatsKeyAsOpaqueValue()
{
AgentSessionId sessionId = new("agentA", "@dafx-agentB@some-key");
Assert.Equal("agentA", sessionId.Name);
Assert.Equal("@dafx-agentB@some-key", sessionId.Key);
}
}
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DurableTask.UnitTests;
public sealed class DurableAIAgentProxyTests
{
// Verifies the proxy rejects a session whose agent name differs from its own,
// and that the durable client is never called when this happens.
[Fact]
public async Task RunAsync_ThrowsWhenSessionBelongsToDifferentAgent()
{
StubDurableAgentClient client = new();
DurableAIAgentProxy proxy = new("agentA", client);
DurableAgentSession session = new(new AgentSessionId("agentB", "shared-key"));
ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>(() =>
proxy.RunAsync(new ChatMessage(ChatRole.User, "hello"), session));
Assert.Equal("session", ex.ParamName);
Assert.Contains("agentB", ex.Message, StringComparison.Ordinal);
Assert.Contains("agentA", ex.Message, StringComparison.Ordinal);
Assert.Equal(0, client.CallCount);
}
// Control test: when the session's agent name matches the proxy's name,
// the request is forwarded to the durable client.
[Fact]
public async Task RunAsync_AllowsSessionWhenAgentNameMatches()
{
AgentSessionId sessionId = new("agentA", "shared-key");
InvalidOperationException sentinel = new("reached the client");
StubDurableAgentClient client = new() { Throw = sentinel };
DurableAIAgentProxy proxy = new("agentA", client);
DurableAgentSession session = new(sessionId);
// Reaching the durable client (and therefore propagating the sentinel) proves the
// name-matching guard accepted this session.
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
proxy.RunAsync(new ChatMessage(ChatRole.User, "hello"), session));
Assert.Same(sentinel, ex);
Assert.Equal(1, client.CallCount);
Assert.Equal(sessionId, client.LastSessionId);
}
// Ensures the agent-name comparison is case-insensitive, so casing differences
// are neither a false-positive rejection nor a bypass.
[Fact]
public async Task RunAsync_AgentNameComparisonIsCaseInsensitive()
{
AgentSessionId sessionId = new("AGENTA", "shared-key");
InvalidOperationException sentinel = new("reached the client");
StubDurableAgentClient client = new() { Throw = sentinel };
DurableAIAgentProxy proxy = new("agentA", client);
DurableAgentSession session = new(sessionId);
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
proxy.RunAsync(new ChatMessage(ChatRole.User, "hello"), session));
Assert.Same(sentinel, ex);
Assert.Equal(1, client.CallCount);
}
private sealed class StubDurableAgentClient : IDurableAgentClient
{
public int CallCount { get; private set; }
public AgentSessionId LastSessionId { get; private set; }
public Exception? Throw { get; set; }
public Task<AgentRunHandle> RunAgentAsync(
AgentSessionId sessionId,
RunRequest request,
CancellationToken cancellationToken)
{
this.CallCount++;
this.LastSessionId = sessionId;
if (this.Throw is not null)
{
return Task.FromException<AgentRunHandle>(this.Throw);
}
throw new InvalidOperationException("Test did not configure a response.");
}
}
}