diff --git a/src/Aevatar.AI.Core/Chat/ChatRuntimeRequestBuilder.cs b/src/Aevatar.AI.Core/Chat/ChatRuntimeRequestBuilder.cs index 0e5ff65ad..544229d16 100644 --- a/src/Aevatar.AI.Core/Chat/ChatRuntimeRequestBuilder.cs +++ b/src/Aevatar.AI.Core/Chat/ChatRuntimeRequestBuilder.cs @@ -77,12 +77,14 @@ public static LLMRequest Build( catalogProof?.AssertMatchesExactTools(exactTools ?? []); } + var callerContext = ResolveCallerContext(baseRequest.CallerContext, effectiveToolContext); + return new LLMRequest { Messages = baseRequest.Messages, RequestId = string.IsNullOrWhiteSpace(requestId) ? baseRequest.RequestId : requestId.Trim(), Metadata = AgentToolExecutionContextMapper.StripOwnedControlKeys(mergedMetadata), - CallerContext = baseRequest.CallerContext, + CallerContext = callerContext, ToolContext = effectiveToolContext, RoutingContext = effectiveLlmControl?.ToRoutingContext(baseRequest.RoutingContext) ?? baseRequest.RoutingContext, LlmControl = effectiveLlmControl, @@ -181,6 +183,47 @@ internal static AuthorizationFence CaptureAuthorizationFence(LLMRequest request) return merged; } + private static LLMRequestCallerContext? ResolveCallerContext( + LLMRequestCallerContext? baseCallerContext, + AgentToolExecutionContext toolContext) + { + var credential = string.IsNullOrWhiteSpace(baseCallerContext?.Credentials?.NyxIdBearer) + ? ResolvePromotableNyxIdCredential(toolContext.Credentials) + : baseCallerContext.Credentials.NyxIdBearer.Trim(); + var credentials = string.IsNullOrWhiteSpace(credential) + ? baseCallerContext?.Credentials + : new LLMRequestCallerCredentials(credential); + if (baseCallerContext is not null) + { + return baseCallerContext with + { + Credentials = credentials, + }; + } + + if (credentials is null && + string.IsNullOrWhiteSpace(toolContext.Caller.ScopeId) && + string.IsNullOrWhiteSpace(toolContext.Caller.OwnerSubject) && + string.IsNullOrWhiteSpace(toolContext.Caller.ResponseId)) + { + return null; + } + + return new LLMRequestCallerContext( + Normalize(toolContext.Caller.ScopeId) ?? string.Empty, + Normalize(toolContext.Caller.OwnerSubject) ?? string.Empty, + Normalize(toolContext.Caller.ResponseId), + credentials); + } + + private static string? ResolvePromotableNyxIdCredential(AgentToolCredentials credentials) => + credentials.NyxIdCredentialKind == AgentToolNyxIdCredentialKind.AgentKey + ? Normalize(credentials.NyxIdAccessToken) + : null; + + private static string? Normalize(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + internal sealed class AuthorizationFence { private readonly IReadOnlyDictionary _schemaTools; diff --git a/test/Aevatar.AI.Tests/ChatRuntimeRequestBuilderCredentialTests.cs b/test/Aevatar.AI.Tests/ChatRuntimeRequestBuilderCredentialTests.cs new file mode 100644 index 000000000..e60173840 --- /dev/null +++ b/test/Aevatar.AI.Tests/ChatRuntimeRequestBuilderCredentialTests.cs @@ -0,0 +1,52 @@ +using Aevatar.AI.Abstractions.LLMProviders; +using Aevatar.AI.Abstractions.ToolProviders; +using Aevatar.AI.Core.Chat; +using FluentAssertions; + +namespace Aevatar.AI.Tests; + +public sealed class ChatRuntimeRequestBuilderCredentialTests +{ + [Fact] + public void Build_WhenToolContextHasAgentKeyCredential_ShouldPromoteCallerBearer() + { + var request = BuildRequest(AgentToolNyxIdCredentialKind.AgentKey); + + request.CallerContext.Should().NotBeNull(); + request.CallerContext!.Credentials.Should().NotBeNull(); + request.CallerContext.Credentials!.NyxIdBearer.Should().Be("nyxid_ag_alpha"); + } + + [Fact] + public void Build_WhenToolContextCredentialKindIsUnspecified_ShouldNotPromoteCallerBearer() + { + var request = BuildRequest(AgentToolNyxIdCredentialKind.Unspecified); + + request.CallerContext.Should().NotBeNull(); + request.CallerContext!.Credentials.Should().BeNull(); + } + + private static LLMRequest BuildRequest(AgentToolNyxIdCredentialKind credentialKind) + { + var toolContext = AgentToolExecutionContext.Empty with + { + Credentials = new AgentToolCredentials( + " nyxid_ag_alpha ", + null, + null, + credentialKind), + Caller = new AgentToolCallerContext(" scope-alpha ", " owner-alpha ", " response-alpha "), + }; + + return ChatRuntimeRequestBuilder.Build( + new LLMRequest + { + Messages = [new ChatMessage { Role = "user", Content = "hello" }], + }, + null, + null, + toolContext, + null, + null); + } +} diff --git a/test/Aevatar.Workflow.Core.Tests/Modules/WorkflowRoleGAgentMappingTests.cs b/test/Aevatar.Workflow.Core.Tests/Modules/WorkflowRoleGAgentMappingTests.cs index bc5d2bc51..1389226a1 100644 --- a/test/Aevatar.Workflow.Core.Tests/Modules/WorkflowRoleGAgentMappingTests.cs +++ b/test/Aevatar.Workflow.Core.Tests/Modules/WorkflowRoleGAgentMappingTests.cs @@ -158,6 +158,9 @@ await agent.HandleWorkflowLlmExecutionIntent(new WorkflowLlmExecutionIntent provider.LastRequest.LlmControl.Should().NotBeNull(); provider.LastRequest.LlmControl!.NyxIdAccessToken.Should().BeNull( "the durable Agent Key should only enter the ephemeral tool context"); + provider.LastRequest.CallerContext.Should().NotBeNull(); + provider.LastRequest.CallerContext!.Credentials.Should().NotBeNull(); + provider.LastRequest.CallerContext.Credentials!.NyxIdBearer.Should().Be(agentKey); } [Fact] @@ -217,6 +220,9 @@ await agent.HandleWorkflowLlmExecutionIntent(new WorkflowLlmExecutionIntent provider.LastRequest.LlmControl.Should().NotBeNull(); provider.LastRequest.LlmControl!.NyxIdAccessToken.Should().BeNull( "the durable Agent Key should only enter the ephemeral tool context"); + provider.LastRequest.CallerContext.Should().NotBeNull(); + provider.LastRequest.CallerContext!.Credentials.Should().NotBeNull(); + provider.LastRequest.CallerContext.Credentials!.NyxIdBearer.Should().Be(agentKey); } [Fact]