.NET: Hosting.Channels minimal core + Responses channel (ADR-0027)#6151
.NET: Hosting.Channels minimal core + Responses channel (ADR-0027)#6151rogerbarreto wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 83%
✓ Security Reliability
The InvocationsChannel exposes two security concerns: (1) internal exception messages are returned verbatim to HTTP clients, risking information disclosure of connection strings, file paths, or internal state; (2) the InvocationsChannelOptions.Allowlist property is defined but never consumed by the handler, meaning an administrator who configures an allowlist for this channel gets silently ignored security configuration. The spec and other channel samples (Sample 4) demonstrate that channels should call context.AuthorizeAsync after extracting identity, but InvocationsChannel bypasses this entirely. The Telegram webhook endpoint accepts unauthenticated POST requests without validating the X-Telegram-Bot-Api-Secret-Token header, allowing anyone who discovers the URL to inject forged Telegram updates to impersonate users or trigger unauthorized agent runs. The FileHostStateStore has a subtle hydration race where _hydrated is set to true before async file loading completes, allowing concurrent callers to observe an empty cache. The primary reliability concern is that the InProcessDurableTaskRunner's single-reader worker loop cannot cancel in-flight task handlers during shutdown. TaskInvocationContext doesn't carry a CancellationToken, and the ResponseRouter's push handler uses CancellationToken.None for downstream I/O. A hanging push operation (network partition, unresponsive endpoint) permanently stalls all subsequent task processing. For an alpha implementation this is acceptable with documentation, but should be tracked for resolution before GA.
✓ Test Coverage
Test coverage has significant gaps for a PR of this scope. The 17 tests cover the allowlist primitives (except AllOfIdentityAllowlist), the in-memory state store, the durable task runner, and the identity linker. However, the most complex production code — AgentFrameworkHost.AuthorizeAsync (80+ line authorization pipeline with 6+ code paths), TelegramChannel (350 lines with group chat, mention detection, webhook/polling, link challenges), FileHostStateStore (277 lines of file I/O with hydration), and the DI wiring — has zero test coverage. The WorkflowInvocationsResponseHook is also a no-op that returns the input unchanged in both branches.
✗ Design Approach
Each issue is tied to a concrete silent misrouting/misclassification path rather than an equivalent implementation choice.
Flagged Issues
- dotnet/src/Microsoft.Agents.AI.Hosting.Channels/Internal/ResponseRouter.cs:73 drops any destination whose channel matches the origin channel, which silently breaks ResponseTarget.Identity/Identities for a different identity on the same channel. The router needs the full originating identity rather than comparing only dest.Identity.Channel.
Automated review by rogerbarreto's agents
Adds docs/specs/003-dotnet-hosting-channels.md, a .NET port of Python's 002-python-hosting-channels.md. Locks 15 design decisions (translate posture, builder-centric composition, Channel + ChannelContribution + capability interfaces, IHostedTargetRunner Foundry-reuse seam, IIdentityAllowlist + IIdentityLinker pipeline, IHostStateStore, IDurableTaskRunner, Generic Host + ASP.NET Core, AgentFrameworkHost naming, per-channel NuGet package layout, IsolationKeys, workflow-agnostic channels, net-new only v1, no hard breaks).
Adds Microsoft.Agents.AI.Hosting.Channels (alpha) with the full v1 type surface from the spec and working in-memory implementations end-to-end on net8 / net9 / net10. Includes Channel + ChannelContribution + capability interfaces, ChannelRequest envelope (full Python parity), ResponseTarget variants, HostedRunResult base + generic, HostedStreamItem family, identity allowlists and linker contracts, link policy + 4 built-ins, IHostStateStore + InMemoryHostStateStore, IDurableTaskRunner + InProcessDurableTaskRunner (two-phase shutdown, retry backoff), AIAgentRunner (working) and WorkflowRunner (skeleton), IsolationKeys plumbing, builder + AddAgentFrameworkHost / MapAgentFrameworkHost extensions. Build is clean with 0 warnings across all 3 target frameworks. FileHostStateStore, OneTimeCodeIdentityLinker, real ScheduleResponseAsync fan-out, channel packages, and FoundryHostedAgentRunner adapter land in follow-up commits.
…onseRouter Rounds out the core scaffold with the three cross-cutting pieces every channel will rely on. FileHostStateStore persists identity registry, link grants, last-seen ledger, continuation tokens, and session aliases as JSON files under HostStatePathOptions; in-memory cache write-through with on-demand hydration. OneTimeCodeIdentityLinker issues short random codes and consumes them via IHostStateStore link grants, no callback routes required. ResponseRouter owns ResponseTarget resolution and durable push fan-out: registers the hosting.push handler on the runner, walks IHostStateStore for Active / AllLinked / Channel(s) / Identities targets, filters via ILinkPolicy, schedules one task per non-originating destination, runs per-destination IChannelResponseHook before IChannelPush.PushAsync, tracks echo_done / response_done idempotency cursors on TaskInvocationContext.State so retries do not double-echo. Build is clean with 0 warnings across all 3 target frameworks.
Adds Microsoft.Agents.AI.Hosting.Channels.Invocations (alpha) implementing the simplest of the three v1 channel packages: a JSON-only invocation surface. POST /invocations/invoke runs the target synchronously and returns the agent text plus session id; GET /invocations/{continuationToken} polls a background run scheduled via background=true in the request body. Uses source-generated JSON via InvocationsJsonContext and the ASP.NET Core request-delegate generator so the surface stays trim/AOT-friendly. Validates the channel contract end-to-end: ChannelContribution routes are wrapped in endpoints.MapGroup(Path), IChannelContext.RunAsync drives the host runner, AgentFrameworkHost.RunInBackgroundAsync writes the ContinuationToken, polling reads it back through IHostStateStore. AddInvocationsChannel hangs off IAgentFrameworkHostBuilder. Build is clean with 0 warnings across all 3 target frameworks. WorkflowInvocationsResponseHook for RequestInfoEvent rendering lands when WorkflowRunner gains end-to-end wiring.
…ndering Replaces the WorkflowRunner stub with a working IHostedTargetRunner that drives the workflow via InProcessExecution.RunStreamingAsync, watches the StreamingRun event stream, accumulates WorkflowOutputEvent payloads, and pauses on RequestInfoEvent. On pause the runner mints a resume token, persists it on IHostStateStore as a ContinuationToken, and surfaces it via ChannelSession.Attributes[workflow.resume_token]. A follow-up request that carries the same attribute (resume) sends an ExternalResponse back to the StreamingRun and continues watching. Adds WorkflowRunResult + WorkflowRunStatus in the core package as the canonical workflow envelope so channels can inspect status without knowing about Workflow internals. InvocationsChannel.WriteSuccessAsync now renders WorkflowRunResult natively: status: awaiting_input with the pending request + resume token, status: completed with concatenated outputs, status: failed with the error message. Adds WorkflowInvocationsResponseHook for non-originating workflow deliveries; the originating reply is rendered by the channel directly per the spec. Build is clean with 0 warnings across all 3 target frameworks.
…g transports
Adds Microsoft.Agents.AI.Hosting.Channels.Telegram (alpha): the second v1 channel package and the showcase for the multi-channel surface. Builds against the raw Telegram Bot API over HttpClient (no Telegram.Bot SDK dependency, so the package stays lean and central package management stays clean). Both transports are supported: polling via a background task driven by the channel's OnStartup hook, and webhook via POST {Path}/webhook. Implements IChannelPush so the host's hosting.push handler can deliver responses to a Telegram chat from any other channel.
Per-conversation isolation derived from ConversationScope: PerUser shares state across DM and group, PerUserPerConversation (default) gives one isolation key per user per chat, PerConversation makes every member of a group share state. Group-message filtering driven by AcceptInGroup: MentionOnly (default for groups), CommandOnly, MentionOrCommand, All. Group-safety rule for link challenges: when the user is in a group conversation and the authorization pipeline returns LinkRequired, the channel sends the challenge to the user's DM and posts a public-safe acknowledgement to the group. Last-seen records updated per inbound message to power ResponseTarget.Active deliveries from peer channels. setMyCommands invoked at startup when RegisterNativeCommands is true. Build is clean with 0 warnings across all 3 target frameworks.
Adds samples/04-hosting/HostingChannels/InvocationsAndTelegram, a runnable WebApplication that mounts one AIAgent on both the Invocations channel (POST /invocations/invoke) and the Telegram channel (long-poll) with OneTimeCodeIdentityLinker wired in, demonstrating cross-channel identity collapse and shared session state. Telegram is skipped at startup when TELEGRAM_BOT_TOKEN is not set. Adds tests/Microsoft.Agents.AI.Hosting.Channels.UnitTests with 17 unit tests covering: ResponseTarget factories and singletons, IIdentityAllowlist tri-state plus AnyOf combinator (Allow short-circuit / Deny wins / all-Abstain), NativeIdAllowlist and LinkedClaimAllowlist glob matching, InMemoryHostStateStore SaveLink with atomic merge plus verified-claim lookup plus link-grant consume plus session alias rotation, OneTimeCodeIdentityLinker round-trip plus auto-merge on verified claim, InProcessDurableTaskRunner schedule with handler invocation plus exponential-backoff retry on failure. All 17 tests pass via Microsoft Testing Platform on net10.0. Also flips AgentFrameworkHostOptions from init-only record to settable class so the Action<TOptions> configure pattern works.
Replaces samples/04-hosting/HostingChannels/InvocationsAndTelegram with two focused samples per the established 04-hosting/ numbering convention: 01_Invocations (smallest possible AddAgentFrameworkHost + AddInvocationsChannel demo with sync run, background run, and polling), 02_Telegram (AddTelegramChannel with polling transport, PerUserPerConversation isolation, MentionOnly group filtering, and a /new ChannelCommand). Each sample stays focused on one channel so readers can copy and adapt without untangling multi-channel composition. Both build clean against net10.0 with 0 warnings. Cross-channel composition stays demonstrable by composing both AddXxxChannel calls on the same host; the cross-channel scenario will land as a dedicated end-to-end sample once the linker UX is polished.
Scopes the .NET hosting channels work to ADR-0027 (minimal hosting core and pluggable channels), mirroring eavan's Python PR microsoft#6580. Everything in ADR-0028 (identity linking, allowlists/authorization, response targeting beyond the originating channel, push/codecs, background and continuation delivery, durable runners, retry/replay, link policy, confidentiality tiers, multicast) is removed from this PR and tracked as follow-up work. The removed code is preserved on a local branch for the ADR-0028 PR. Spec: trims docs/specs/003-dotnet-hosting-channels.md to the minimal core plus the Responses channel, adds a Non-goals for v1 section mirroring ADR-0027, and references ADR 0027 and 0028. Core (Microsoft.Agents.AI.Hosting.Channels): removes 46 ADR-0028 files (allowlists, authorization, identity linker, link policy, durable runner, response target, push, codecs, continuation, last-seen, link grant, conversation scope/context). Slims AgentFrameworkHost (run/stream/reset-session only), IChannelContext, the builder, ChannelRequest, AgentFrameworkHostOptions, and the host/endpoint extensions. IHostStateStore is reduced to reset-session aliases plus workflow checkpoint path derivation. AIAgentRunner now resolves and caches an AgentSession per active session alias so identical isolation keys reuse one session (ADR-0027 continuity gate). WorkflowRunner runs forward and surfaces awaiting-input on a RequestInfoEvent. Responses channel (Microsoft.Agents.AI.Hosting.Channels.Responses): new self-contained channel mapping OpenAI Responses requests and streams onto the host through the IChannelContext run/stream seam. Synchronous JSON response and SSE streaming, run and response hooks, source-generated JSON. Replaces the removed Invocations and Telegram channel packages. Samples: replaces the Invocations and Telegram samples with 01_ResponsesAgent (agent on the Responses channel) and 02_ResponsesWorkflow (workflow on the Responses channel with a run-hook input adapter), mirroring the Python local_responses and local_responses_workflow samples. Tests: drops the allowlist/linker/durable/state-merge tests; adds host state store, workflow runner, host composition, and channel request tests. 8 of 8 pass. All changed projects build clean on Debug net10.0 with warnaserror and pass CI-parity dotnet format verify-no-changes.
c58bf86 to
35a88ae
Compare
…rm hook Two declared seams of the channel contract were not actually invoked. Wiring them so the contract behaves as ADR-0027 describes (host owns route/lifecycle aggregation and invocation of per-channel hooks). Lifecycle: adds ChannelLifecycleRegistry + ChannelLifecycleService (IHostedService). AddAgentFrameworkHost registers the service; MapAgentFrameworkHost records each contribution's OnStartup/OnShutdown into the registry. StartAsync invokes OnStartup; StopAsync invokes OnShutdown in reverse registration order. Stream-transform hook: adds StreamTransformHook to ResponsesChannelOptions and applies it in the SSE path, so the host-consumed AgentResponseUpdate stream passes through IChannelStreamTransformHook before the channel renders response.output_text.delta frames.
…ontract Adds Microsoft.Agents.AI.Hosting.Channels.IntegrationTests: 20 fast, hermetic E2E tests that spin up an ASP.NET Core TestServer hosting an AgentFrameworkHost over deterministic fake agents and a trivial workflow (no live model, no infra; whole suite runs in ~1s). Contract tests (via a test-only ProbeChannel): routes mount under Channel.Path, custom path honored, OnStartup/OnShutdown lifecycle callbacks fire, endpoint filters apply to the channel group, IChannelContext.RunAsync drives the target, IChannelContext.StreamAsync yields updates then exactly one HostedStreamCompleted, multiple channels share one host and target, and the same channel drives both an AIAgent and a Workflow target (target neutrality). Hook tests (Responses channel): run hook rewrites input before the target, response hook rewrites the result before serialization, stream-transform hook injects a chunk while streaming. Session continuity (ADR-0027 gate): identical isolation key (stamped by a header-reading run hook simulating trusted middleware) reuses one cached AgentSession (CountingAgent returns 1 then 2); different keys partition; ResetSessionAsync rotates the alias to a fresh session. Responses protocol: sync string input and input-item array parsing, SSE created/delta/completed sequence, missing input -> 400, malformed JSON -> 400, workflow target -> 200. All changed projects build clean with warnaserror and pass CI-parity dotnet format verify-no-changes; 20 integration + 8 unit tests green.
…rkflows via Responses Adds two more end-to-end scenarios through the Responses channel, both hermetic (no live model). Function calling: a ChatClientAgent built with a tool over a deterministic two-turn FakeFunctionCallingChatClient (turn 1 emits a FunctionCallContent; turn 2, after the framework's FunctionInvokingChatClient executes the tool and feeds back the FunctionResultContent, emits the final answer). Driven via POST /responses, the test asserts the server-side tool actually ran and the post-tool answer is rendered, plus a streaming variant asserting the final answer arrives over SSE. Agent workflow: AgentWorkflowBuilder.BuildSequential over two deterministic agents hosted through the Responses channel, with a run hook adapting the parsed Responses input into the workflow's ChatMessage-list input; the host's WorkflowRunner drives it and a completed Responses object is returned. Integration suite now 23 tests; all build warnaserror-clean, pass CI-parity dotnet format verify-no-changes, and run in under a second.
… function-calling tests
The earlier function-calling test used only a parameterless tool (after a binding fix). Adds explicit coverage for both shapes through POST /responses: ParameterlessTool_IsInvoked asserts a no-arg tool runs and the post-tool answer is rendered; ParameterizedTool_ReceivesArgument has the fake chat client supply a 'city' argument on the function call and asserts the bound value ('Seattle') actually reached the tool body. FakeFunctionCallingChatClient now accepts an optional argument map. Integration suite is 24 tests, all green, warnaserror-clean and format-clean.
….OpenAI The Responses-channel samples don't need any Azure-specific features, so depend on the OpenAI SDK directly. Sample 01 now builds its chat client from OPENAI_API_KEY/OPENAI_MODEL via OpenAIClient instead of AzureOpenAIClient + DefaultAzureCredential. Sample 02 (echo workflow) dropped its unused Azure.AI.OpenAI, Azure.Identity, and Microsoft.Agents.AI.OpenAI references.
Address PR review feedback on the public API shape of the package so it is
netstandard2.0-friendly and follows the MAF mutable-class convention.
Records -> classes:
- Convert all 17 record types to plain classes, dropping the record, init,
and required keywords entirely. Non-nullable required members become
constructor parameters (get-only); value-type/nullable/defaulted members
stay { get; set; }. Replace the 7 'with' expressions with copy
constructors on ChannelRequest and ChannelSession.
Interface trim (keep only genuine multi-impl / mix-in seams):
- Collapse IChannelContext into a sealed ChannelContext class with an
internal constructor: the host owns construction and channels only
consume it, so the interface added nothing.
- Remove the unused IIsolationKeysAccessor and the dead IsolationKeys type
(registered but never consumed in the ADR-0027 scope; session isolation
is carried by ChannelSession.IsolationKey).
- Keep IHostStateStore, IHostedTargetRunner, and the IChannelRunHook /
IChannelResponseHook / IChannelStreamTransformHook capability interfaces
(the latter are mixed into Channel subclasses) and IAgentFrameworkHostBuilder.
Also remove an unused using in ChannelLifecycleService flagged by format.
Summary
.NET hosting channels, scoped to ADR-0027 (minimal hosting core and pluggable channels), mirroring eavan's Python PR #6580. Ships the channel-neutral host core plus the Responses channel, two samples, and unit tests. Everything in ADR-0028 (linking, authorization, multicast, push, background/continuation, durable runners) is out of scope and tracked as follow-up.
This PR was reduced from an earlier broad draft; the removed ADR-0028 code is preserved on a local branch for the follow-up PR.
In scope (ADR-0027)
Core:
Microsoft.Agents.AI.Hosting.ChannelsAgentFrameworkHost(run / stream /ResetSessionAsync),IAgentFrameworkHostBuilder,AddAgentFrameworkHost(Microsoft.Extensions.Hosting) +MapAgentFrameworkHost(Microsoft.AspNetCore.Builder).Channel+ChannelContribution(routes, endpoint filters, commands, lifecycle) +ChannelCommand/ChannelCommandContext.ChannelRequest(channel-neutral envelope),ChannelSession(IsolationKey)for explicit session continuity,ChannelIdentityas request metadata only.IChannelRunHook,IChannelResponseHook,IChannelStreamTransformHook.IHostedTargetRunner+AIAgentRunner(resolves and caches anAgentSessionper active session alias so identical isolation keys reuse one session) +WorkflowRunner(runs forward, surfaces awaiting-input onRequestInfoEvent).IHostStateStorelimited to reset-session aliases + workflow checkpoint path derivation;InMemoryHostStateStore+FileHostStateStore.IsolationKeys+IIsolationKeysAccessor(Foundry-flag-gated header lift).Channel:
Microsoft.Agents.AI.Hosting.Channels.ResponsesResponsesChannelmapping OpenAI Responses requests and streams onto the host through theIChannelContextrun/stream seam: synchronous JSON response and SSE streaming, run + response hooks, source-generated JSON.Samples (
samples/04-hosting/HostingChannels/)01_ResponsesAgent(agent on the Responses channel; sync + streaming).02_ResponsesWorkflow(workflow on the Responses channel with anIChannelRunHookinput adapter).Tests (
Microsoft.Agents.AI.Hosting.Channels.UnitTests)Out of scope (ADR-0028, follow-up PRs)
Identity linking, allowlists / authorization, response routing beyond the originating channel (
ResponseTarget, active, all-linked), push / payload codecs, background + continuation tokens, durable task runners, retry / replay, link policy, confidentiality tiers, multicast, and the Telegram / Invocations / Discord / Activity channel packages.Local validation
--warnaserror-> 0 warnings, 0 errors.dotnet format --verify-no-changesinsidemcr.microsoft.com/dotnet/sdk:10.0(CI parity) across the changed src + test projects -> clean.Reviewer notes
docs/specs/003-dotnet-hosting-channels.mdis trimmed to the ADR-0027 boundary with an explicit "Non-goals for v1 (deferred to ADR-0028)" section.Hosting.OpenAI,Hosting.A2A,Hosting.AGUI.AspNetCore,Hosting.AzureFunctions,Foundry.Hosting. Net-new only.