Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/decisions/0026-hosted-session-identity-context.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
---
status: accepted
status: superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md)
contact: rogerbarreto
date: 2026-05-07
date: 2026-06-29
deciders: rogerbarreto
consulted: []
informed: []
---

# Hosted session identity context for Foundry Hosting

> **Superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).** `Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) replaced `ResponseContext.Isolation` (`UserIsolationKey` / `ChatIsolationKey`, headers `x-agent-user-isolation-key` / `x-agent-chat-isolation-key`) with `ResponseContext.PlatformContext` (`UserIdKey` / `CallId`, headers `x-agent-user-id` / `x-agent-foundry-call-id`). The chat isolation key was removed and `HostedSessionContext` is now user-only. This ADR is retained as the historical record of the original design.

## Context and Problem Statement

Server-hosted Foundry agents need a way to scope per-user state (most notably `FoundryMemoryProvider` memories) by the end user that initiated the request. The Foundry platform already injects `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers on every Responses request, but the agent-framework hosting layer did not surface those values to `AIContextProvider` instances. The provider's `stateInitializer` only received an `AgentSession?` with no identity attached, so per-user scoping was impossible without out-of-band plumbing.
Expand Down
84 changes: 84 additions & 0 deletions docs/decisions/0030-hosted-platform-context-agentserver-2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
status: accepted
contact: rogerbarreto
date: 2026-06-29
deciders: rogerbarreto
consulted: []
informed: []
---

# Hosted platform context (user id + call id) for Foundry Hosting on AgentServer 2.0

Supersedes [ADR-0026](0026-hosted-session-identity-context.md).

## Context and Problem Statement

[ADR-0026](0026-hosted-session-identity-context.md) sourced the hosted-agent end-user identity from `ResponseContext.Isolation` (an `IsolationContext` typed `UserIsolationKey` / `ChatIsolationKey`), injected by the platform as the `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers.

`Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) removes that surface. `ResponseContext.Isolation` is gone; the platform now exposes `ResponseContext.PlatformContext` (a `PlatformContext` typed `UserIdKey` and `CallId`), populated from the `x-agent-user-id` and `x-agent-foundry-call-id` headers. The chat isolation key no longer exists, and a new per-request **call id** is introduced that first-party Foundry services (the toolbox proxy in particular) require on outbound calls to resolve the server-side-stored caller context. The hosting layer in `Microsoft.Agents.AI.Foundry.Hosting` had to migrate to this contract without changing the public shape that samples and providers depend on.

## Decision Drivers

- Track the breaking `Azure.AI.AgentServer.*` 2.0.0 surface (`PlatformContext` replacing `Isolation`) while keeping the same per-user partitioning guarantees from ADR-0026.
- Keep the change **internal**: existing hosted samples and `AIContextProvider`s must not need code changes. `session.GetHostedContext().UserId`, `HostedSessionIsolationKeyProvider`, and `AddFoundryResponses` stay source-compatible.
- Forward the new per-request call id verbatim on outbound calls to Foundry first-party services so per-user toolbox OAuth consent and other server-side caller-context lookups keep working.
- Remain resilient on protocol `1.0.0`: when only the legacy headers are present, `UserIdKey` still resolves and `CallId` is simply absent.
- Preserve the strict-resume tamper defense from ADR-0026 with identity now reduced to user only.

## Considered Options

For the identity source:

1. **Map `ResponseContext.PlatformContext.UserIdKey`** into the existing `HostedSessionContext` (user only), keeping ADR-0026's storage shape and read accessor.
2. Keep a `ChatId` slot on `HostedSessionContext` for backward source-compatibility, populated from `CallId` or left null.

For the call id propagation:

A. **A request-scoped ambient (`HostedCallContext`, an `AsyncLocal<string?>`)** set by the handler and re-applied before each egress point, read by the outbound delegating handler.
B. Thread the call id through every method signature down to the toolbox bearer handler.

For session keying (previously implied by the conversation/chat pairing):

I. **`HostedConversationKey`** resolving a stable partition from `conversation_id ?? partition(previous_response_id) ?? partition(responseId)`.
II. Continue keying on the container session id (`FOUNDRY_AGENT_SESSION_ID`).

## Decision Outcome

Chosen: **Option 1** for identity, **Option A** for call id, **Option I** for session keying.

Rationale:

- **`ChatId` dropped (Option 2 rejected).** The platform no longer supplies a chat key; carrying a synthetic one would invent identity the trust boundary does not provide. `HostedSessionContext` becomes user-only (`HostedSessionContext(string userId)` / `UserId`), and the strict-resume check validates `UserId` alone. The corresponding `HostedFoundryMemoryProviderScopes` values `PerChat` and `PerUserAndChat` are removed; `PerUser` is retained.
- **Ambient call id (Option B rejected).** Writing `HostedCallContext.CallId` inside the streaming `async IAsyncEnumerable` iterator is reverted across each `yield`, so a single up-front assignment is lost before the toolbox/MCP egress runs. The handler therefore captures `context.PlatformContext?.CallId` once and **re-applies it immediately before each egress point**; `FoundryToolboxBearerTokenHandler` forwards it as `x-agent-foundry-call-id`. The ambient is request-scoped and never leaks into the caller's execution context (guarded by a unit test).
- **`HostedConversationKey` (Option II rejected).** One container serves many conversations for its lifetime, so the container session id cannot key per-conversation state. The partition key is derived from the conversation/`previous_response_id`/minted response id instead.

Implementation summary in `Microsoft.Agents.AI.Foundry.Hosting`:

| Type | Visibility | Change vs ADR-0026 |
|---|---|---|
| `HostedSessionContext` | public sealed | Now user-only (`UserId`); `ChatId` removed. |
| `PlatformHostedSessionIsolationKeyProvider` | internal sealed | Maps `context.PlatformContext.UserIdKey` (was `context.Isolation.UserIsolationKey` / `ChatIsolationKey`). |
| `HostedCallContext` | internal static | New. Request-scoped `AsyncLocal<string?>` holding the `x-agent-foundry-call-id` value. |
| `HostedConversationKey` | internal | New. Resolves the per-conversation partition key. |
| `FoundryToolboxBearerTokenHandler` | internal | Now also forwards `x-agent-foundry-call-id` outbound. |
| `HostedFoundryMemoryProviderScopes` | public | `PerChat` / `PerUserAndChat` removed; `PerUser` kept. |

Package manifests bump the responses container protocol to `2.0.0` (invocations stays `1.0.0`).

## Consequences

Positive:

- Per-user memory partitioning and the strict-resume tamper defense from ADR-0026 are preserved with no public API churn for samples or providers.
- Per-user toolbox OAuth consent and other server-side caller-context lookups keep working because the per-request call id is forwarded on egress.
- Works unchanged on protocol `1.0.0` (no call id) and `2.0.0`.

Negative:

- `HostedSessionContext.ChatId` and the `PerChat` / `PerUserAndChat` memory scopes are removed; any out-of-tree consumer that referenced them must move to user-scoped partitioning.
- The call id must be re-applied before every egress point because of the async-iterator `AsyncLocal` revert; a missed re-apply silently drops the header. This is covered by unit tests.

## Out of scope

- HMAC tamper signatures over the persisted context remain unimplemented; equality comparison against `ResponseContext.PlatformContext` on every request is sufficient because the platform sets the header at the trust boundary.
- The per-request `User` field on `CreateResponse` is still intentionally not consumed.
6 changes: 3 additions & 3 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<PackageVersion Include="MessagePack" Version="3.1.7" /> <!-- Transitive dependency of Aspire pinned to newer version due to vulnerability in 2.5.192 -->
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.25" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.26" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.3" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
Expand All @@ -8,7 +8,6 @@ SKILL_NAMES=support-style,escalation-policy
# In production, skills are provisioned externally — leave this unset or false.
PROVISION_SAMPLE_SKILLS=true
AZURE_BEARER_TOKEN=DefaultAzureCredential
# When running outside the Foundry platform the platform-injected isolation keys are absent.
# These two variables provide fallback values for local Docker debugging only.
HOSTED_USER_ISOLATION_KEY=local-dev-user
HOSTED_CHAT_ISOLATION_KEY=local-dev-chat
# When running outside the Foundry platform the platform-injected user-identity key is absent.
# This variable provides a fallback value for local Docker debugging only.
HOSTED_USER_ISOLATION_KEY=local-dev-user
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Dockerfile for contributors building from the agent-framework repository source.
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
Expand All @@ -10,7 +10,6 @@
# docker run --rm -p 8088:8088 \
# -e AGENT_NAME=hosted-agent-skills \
# -e HOSTED_USER_ISOLATION_KEY=alice \
# -e HOSTED_CHAT_ISOLATION_KEY=alice-chat-1 \
# --env-file .env hosted-agent-skills
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
Expand All @@ -20,4 +19,4 @@ WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-agent-skills
displayName: "Hosted Agent Skills"

Expand All @@ -21,7 +21,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-agent-skills
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#requires -Version 7
#requires -Version 7
<#
.SYNOPSIS
Local smoke test for the Hosted-AgentSkills sample.
Expand Down Expand Up @@ -51,7 +51,6 @@ function Start-Container {
-e AGENT_NAME=hosted-agent-skills `
-e AZURE_BEARER_TOKEN=$bearer `
-e HOSTED_USER_ISOLATION_KEY=smoke-user `
-e HOSTED_CHAT_ISOLATION_KEY=smoke-chat-1 `
--env-file .env `
$ImageName | Out-Host
if ($LASTEXITCODE -ne 0) { throw "docker run failed." }
Expand Down Expand Up @@ -97,4 +96,4 @@ try {
}
finally {
docker rm -f $ContainerName 2>$null | Out-Null
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-azure-search-rag
displayName: "Hosted Azure AI Search RAG Agent"

Expand All @@ -22,7 +22,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-azure-search-rag
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-chat-client-agent
displayName: "Hosted Chat Client Agent"

Expand All @@ -19,7 +19,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-chat-client-agent
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-files
displayName: "Hosted Files Agent"

Expand All @@ -21,7 +21,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-files
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-foundry-agent
displayName: "Hosted Foundry Agent"

Expand All @@ -19,7 +19,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-foundry-agent
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-local-codeact
displayName: "Hosted Local CodeAct Agent"

Expand All @@ -21,7 +21,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.5"
memory: 1Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-local-codeact
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.5"
memory: 1Gi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-local-tools
displayName: "Seattle Hotel Agent with Local Tools"

Expand All @@ -20,7 +20,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-local-tools
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: mcp-tools
displayName: "MCP Tools Agent"

Expand All @@ -21,7 +21,7 @@ template:
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: mcp-tools
protocols:
- protocol: responses
version: 1.0.0
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
Loading
Loading