Skip to content

.NET: Scope OpenAI Responses conversation access to the owning agent#6892

Draft
rogerbarreto wants to merge 1 commit into
microsoft:mainfrom
rogerbarreto:rogerbarreto/openai-responses-conversation-scoping-119983
Draft

.NET: Scope OpenAI Responses conversation access to the owning agent#6892
rogerbarreto wants to merge 1 commit into
microsoft:mainfrom
rogerbarreto:rogerbarreto/openai-responses-conversation-scoping-119983

Conversation

@rogerbarreto

Copy link
Copy Markdown
Member

Motivation & Context

The in-memory OpenAI Responses hosting service resolved conversation history from a
client-supplied conversation identifier and read/appended to that conversation without
checking that it belongs to the agent scope serving the request. Each agent is mapped to its
own responses endpoint (/{agentName}/v1/responses) while the conversation store is a shared
singleton, so a request sent to one agent''s endpoint could reference a conversation owned by
a different agent and be served silently. This tightens conversation access to the owning
agent scope as a defense-in-depth / consistency improvement.

Description & Review Guide

  • What are the major changes?

    • InMemoryResponsesService now accepts an optional owning-agent scope via a new,
      backward-compatible constructor overload (existing constructors and DI registration pass
      null and are unaffected).
    • ValidateRequestAsync performs a fail-closed ownership check before any conversation
      read/append: a supplied conversation whose stored owner (metadata["agent_id"]) does not
      match the endpoint''s agent scope is rejected. Unknown or unowned conversations keep their
      existing behavior.
    • The per-agent MapOpenAIResponses overload wires the trusted route agent.Name as the
      owner scope (the client-supplied agent field is not used for scoping).
    • Added InMemoryResponsesServiceTests covering cross-agent, same-agent, unowned, and
      no-scope cases.
  • What is the impact of these changes?

    • A mismatched, non-owned conversation identifier is now rejected (HTTP 400 via the existing
      validation path) instead of being served. No public API breaks; the new constructor
      parameter is optional.
    • Owner comparison is ordinal (case-sensitive), consistent with how agent identity is keyed
      elsewhere in this subsystem (the per-agent conversation index uses ordinal MemoryCache
      keys and agents resolve via ordinal keyed-service keys), so case-only-different agent names
      remain distinct owners.
  • What do you want reviewers to focus on?

    • Whether unowned/unknown conversations should also be rejected at validation time, or left
      to the existing execution path as done here.

Related Issue

No public issue is linked. This is a self-contained defense-in-depth / access-scoping
consistency change; it is tracked outside the public tracker. Author to attach an internal
tracking reference if applicable.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

The in-memory Responses hosting service resolved conversation history by a
client-supplied conversation identifier and read/appended to it without
verifying the conversation belonged to the agent scope serving the request.
Because each agent has its own responses endpoint but shares the conversation
store, a request could reference a conversation owned by a different agent.

Reject a conversation identifier whose stored owner (metadata "agent_id")
does not match the endpoint's agent scope, before any read or append. Unknown
or unowned conversations keep their existing behavior. Add unit tests covering
cross-agent, same-agent, unowned, and no-scope cases.
Copilot AI review requested due to automatic review settings July 2, 2026 16:51
@rogerbarreto rogerbarreto self-assigned this Jul 2, 2026
@giles17 giles17 added the .NET Usage: [Issues, PRs], Target: .Net label Jul 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens the in-memory OpenAI Responses hosting implementation so that a client-supplied conversation ID can only be used on the agent-scoped endpoint that owns it (based on metadata["agent_id"]), preventing silent cross-agent conversation reuse.

Changes:

  • Added an optional ownerAgentId scope to InMemoryResponsesService and enforced an ownership check during ValidateRequestAsync.
  • Wired per-agent MapOpenAIResponses to pass the trusted route agent name as the scope.
  • Added unit tests covering cross-agent mismatch, same-agent, unowned, and no-scope scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs Adds owner scoping and a validation-time check rejecting conversations owned by a different agent.
dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs Passes agent.Name as the trusted owner scope when mapping per-agent Responses endpoints.
dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryResponsesServiceTests.cs Introduces tests for the new conversation ownership validation behavior.

Comment on lines +209 to +213
if (conversation is not null
&& conversation.Metadata.TryGetValue(AgentIdMetadataKey, out var ownerAgentId)
&& !string.IsNullOrEmpty(ownerAgentId)
&& !string.Equals(ownerAgentId, this._ownerAgentId, StringComparison.Ordinal))
{
Comment on lines +41 to +42
// Assert
Assert.NotNull(error);

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 90%

✓ Correctness

The PR correctly implements conversation access scoping for the InMemoryResponsesService. The logic in ValidateConversationScopeAsync is sound: it properly null-checks the owner scope, conversation storage, conversation ID, and the fetched conversation before performing the ordinal comparison. The Conversation.Metadata property is initialized with an empty dictionary (never null), making the TryGetValue call safe. The constructor chaining preserves backward compatibility. The tests adequately cover the key scenarios (cross-agent rejection, same-agent pass, unowned pass, no-scope pass). No correctness issues found.

✓ Security Reliability

This PR adds proper conversation access scoping to prevent cross-agent conversation access. The implementation is sound: validation correctly rejects conversations owned by other agents, the PreviousResponseId path is implicitly scoped by the per-instance response cache, null checks are consistent between validation and execution paths, and error messages avoid leaking sensitive details. No significant security or reliability issues found.

✓ Test Coverage

The test suite covers the four primary validation paths (cross-agent rejection, same-agent pass, unowned pass, no-scope pass) but omits a test for the explicitly-documented case-sensitive comparison behavior and a test for nonexistent conversation IDs. Assertions in the cross-agent test only check non-null without verifying the error code/message.

✓ Failure Modes

The PR correctly implements conversation ownership scoping. The validation logic is properly positioned in the request flow, handles null/empty cases appropriately, and the fail-open behavior for unowned conversations is intentional and well-documented. No concrete silent-failure paths or operational failure modes were identified in the changed code.

✗ Design Approach

I found one blocking design issue: the new ownership check relies on conversation.Metadata["agent_id"], but that field remains fully client-mutable through the public conversation update route, so the new scoping can be bypassed by reassigning a conversation's owner before calling another agent's responses endpoint.

Flagged Issues

  • The ownership check trusts conversation.Metadata["agent_id"], but ConversationsHttpHandler.UpdateConversationAsync replaces Metadata from the request (dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs:159-164). A client can rewrite a conversation's owner from agent B to agent A and then satisfy the new validation in InMemoryResponsesService, defeating the stated goal of scoping conversation access to the owning agent.

Automated review by rogerbarreto's agents

// keyed-service keys, so case-only-different names are distinct agents. Compare ordinally
// to stay consistent and avoid treating a different agent as the same owner.
if (conversation is not null
&& conversation.Metadata.TryGetValue(AgentIdMetadataKey, out var ownerAgentId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trusts conversation.Metadata["agent_id"] as the authoritative owner, but that value is client-mutable: ConversationsHttpHandler.UpdateConversationAsync overwrites Metadata = request.Metadata. A caller can update a conversation created for agent B so its metadata says agent A, then this check passes and /{agentA}/v1/responses will read/append that history. Ownership needs to come from server-controlled state or agent_id updates must be rejected.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Flagged issue

The ownership check trusts conversation.Metadata["agent_id"], but ConversationsHttpHandler.UpdateConversationAsync replaces Metadata from the request (dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs:159-164). A client can rewrite a conversation's owner from agent B to agent A and then satisfy the new validation in InMemoryResponsesService, defeating the stated goal of scoping conversation access to the owning agent.


Source: automated DevFlow PR review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants