Skip to content

Fix MEAI001 leak from experimental RequiresConfirmation in source-generated AIContent contexts - #22

Open
jeffhandley wants to merge 1 commit into
mainfrom
jeffhandley/fix-ai-json-experimental-leak
Open

Fix MEAI001 leak from experimental RequiresConfirmation in source-generated AIContent contexts#22
jeffhandley wants to merge 1 commit into
mainfrom
jeffhandley/fix-ai-json-experimental-leak

Conversation

@jeffhandley

@jeffhandley jeffhandley commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Tracks dotnet#7658

Problem

ToolApprovalRequestContent.RequiresConfirmation is [Experimental(MEAI001)], while ToolApprovalRequestContent is a stable, statically registered [JsonDerivedType] of the [JsonPolymorphic] AIContent and InputRequestContent hierarchies. This regression was introduced in 10.8.0 by Add ToolApprovalRequestContent.RequiresConfirmation (#7549).

As a result, System.Text.Json source generation emits the experimental member into the JSON metadata for any consumer context that reaches AIContent, even when the consumer never uses approval APIs:

[JsonSerializable(typeof(List<AIContent>))]
internal partial class JsonContext : JsonSerializerContext;

The build emits MEAI001 from source-generated code. Consumers are then forced into a project-wide suppression as the only practical remedy, but the suppression does not remove the member from their generated contract: their externally exposed JSON payloads still include the experimental requiresConfirmation field without an approval-feature opt-in.

Fix

ToolApprovalRequestContent is stable; only RequiresConfirmation is experimental. Keep the type in the static polymorphic contract, but gate its serialization at the member level using the established pattern used by UsageDetails token counts and HostedFileContent:

[Experimental(...)]
[JsonIgnore]
public bool RequiresConfirmation
{
    get => RequiresConfirmationCore;
    set => RequiresConfirmationCore = value;
}

[JsonInclude]
[JsonPropertyName("requiresConfirmation")]
internal bool RequiresConfirmationCore { get; set; } = true;

The internal member remains available to the package-owned default serialization path, so persisted conversations continue to round-trip requiresConfirmation. Consumer source-generated contracts omit the experimental field, and direct use of the public property continues to require MEAI001 opt-in.

The two Microsoft.Extensions.AI.Evaluation.Reporting projects had gained <NoWarn>$(NoWarn);MEAI001</NoWarn> alongside dotnet#7549 solely because of this leak. The fix removes both suppressions; their clean builds demonstrate that they were leak-driven. Abstractions and AI retain their suppressions because they legitimately use experimental APIs directly.

Regression coverage

Adds a no-suppression consumer-boundary guard in Microsoft.Extensions.AI.Stabilization.Tests:

  • AIContentSerializationRegressionContext.cs source-generates List<AIContent>. Any experimental member leaking into the contract produces MEAI001 at compile time.
  • AIContentSerializationRegressionTests.cs confirms stable List<AIContent> round-trips through that generated context.
  • ToolApprovalRequestContent tests verify RequiresConfirmation still round-trips as both true and false through AIJsonUtilities.DefaultOptions and AIContent polymorphism.

Validation

  • Microsoft.Extensions.AI.Stabilization.Tests: 90/90 pass across net472, net8.0, net9.0, and net10.0 without an MEAI001 suppression.
  • Microsoft.Extensions.AI.Abstractions.Tests: 1634 pass; the intended persistence round-trip remains intact.
  • Negative proof: restoring the original public serialized property produces MEAI001 in the generated AIContentSerializationRegressionContext.ToolApprovalRequestContent.g.cs across all four target frameworks.

…erated AIContent contexts

ToolApprovalRequestContent.RequiresConfirmation is [Experimental(MEAI001)] but the
type is a stable, statically-registered [JsonDerivedType] of the [JsonPolymorphic]
AIContent. System.Text.Json's source generator therefore emits the experimental member
into the JSON metadata of any consumer whose JsonSerializerContext includes AIContent
(e.g. List<AIContent>), forcing that consumer to suppress MEAI001 even when it never
uses approval APIs.

Route serialization through a non-experimental internal member: the public property is
now [JsonIgnore] and delegates to internal RequiresConfirmationCore, which carries
[JsonInclude] and [JsonPropertyName("requiresConfirmation")] so the value still
round-trips through the package's default serialization options. This mirrors the
existing pattern on UsageDetails token counts and HostedFileContent.

Also removes the now-unnecessary MEAI001 suppressions from the two Evaluation.Reporting
projects, which had gained them solely because of this leak (PR dotnet#7549).

Adds a no-suppression, MEAI001-as-error regression guard in the Stabilization tests: a
source-generated context over List<AIContent> that fails to compile if any experimental
member leaks back into consumer metadata.

Fixes dotnet#7658

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant