Summary
Microsoft.Agents.AI.Workflows.Declarative implements a protected-value
control for declarative agent workflows: values assigned from the
Environment scope carry SensitivityLevel.Sensitive, and every executor
that evaluates an expression destined for an external call checks the
evaluated result’s Sensitivity before transmitting. The check covers the
parameters of those calls one by one — agent name and version, structured
inputs, input messages, function names and arguments, MCP server URLs/labels/
tool names/arguments/headers, HTTP methods/URLs/headers/bodies — except
conversationId, which every conversation-aware executor evaluates through
the same expression engine and forwards unchecked.
The value is not merely a local identifier. The shipped Foundry provider puts
it on the wire: as ChatOptions.ConversationId inside the streaming agent
request, and as the target conversation of
CreateProjectConversationItemsAsync message writes. A workflow author who
references an Env-scoped value in a conversationId expression therefore
causes that value to be transmitted to the configured agent service — the
exact transmission the guard on every neighboring parameter exists to
prevent. The omission applied equally to the connectionName parameter of
the MCP and HTTP actions.
The coverage gap was reported to Microsoft on 2026-09-30 and fixed upstream
as PR #9071 (“Reject sensitive declarative identifiers”), which routes
conversationId, connectionName and the conversation-retrieval
identifiers through a single checked accessor in the executor base class.
Background
Declarative workflows let an application describe an agent orchestration in
YAML and run it through Microsoft.Agents.AI.Workflows.Declarative. Workflow
expressions evaluate in a scoped engine: values from the workflow’s own state
are ordinary, while values provided by the host under the Environment scope
— connection strings, keys, host configuration — are tagged
SensitivityLevel.Sensitive.
The guard built on those tags is deliberate: an executor that would hand an evaluated value to an external system throws rather than transmit when the value is sensitive. The guard had been extended over several rounds during September 2026 as sibling omissions were closed in the HTTP request executor, the MCP tool executor, and the invocation executors — in each round, the fix added checks to the parameters that particular investigation had identified. The mechanism is a per-parameter hand enumeration, and that is where coverage gaps live.
The gap
Each executor evaluates conversationId through the same Evaluator.GetValue
call that computes Sensitivity, and simply never looks at it:
private string? GetConversationId()
{
if (this.Model.ConversationId is null)
{
return null;
}
// Evaluated through the same engine as every guarded parameter below…
string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value;
// …but .Sensitivity is never inspected before the value is returned.
return conversationId;
}
Compare the sibling parameter in the same file, which captures the
EvaluationResult and refuses sensitive values:
// agent name — guarded
EvaluationResult<string> result = this.Evaluator.GetValue(this.Model.AgentName);
if (result.Sensitivity == SensitivityLevel.Sensitive)
{
throw new DeclarativeActionException("Cannot send sensitive agent name ...");
}
The same unguarded GetConversationId() pattern existed in six executors —
InvokeAzureAgent, InvokeFunctionTool, InvokeMcpTool, HttpRequest,
AddConversationMessage, CopyConversationMessages — plus GetConnectionName()
in the MCP and HTTP executors. The consumed value flows straight into the
provider boundary (agentProvider.InvokeAgentAsync(..., conversationId, ...)
and agentProvider.CreateMessageAsync(conversationId, ...)), which, with the
shipped Foundry provider, is on the wire.
Verifying it
The differential was straightforward because the control next door is exact:
a sensitive Env value placed in agentName throws
(“Cannot send sensitive agent name”) before the provider is invoked; the same
value placed in conversationId reaches the provider unchanged — verified
with the provider boundary mocked, asserting on the value the executor passes
it. A battery of 23 expression shapes that reference the protected value
(records, tables, lambdas, interpolations, field access) were all blocked in
function arguments, confirming the expression walker itself was thorough —
the gap was in which parameters were routed through it, not in the analysis.
After the fix, the same two probes flip: the former leak cases now throw
Cannot use a sensitive value for conversation ID in InvokeAzureAgent /
... in InvokeFunctionTool, and the controls (including the vendor’s own
sensitive-value test suite) pass unchanged. The same differential was re-run
against the published 1.24.0 package before this writeup went up — same
results.
Impact
Scope, stated plainly: the value is transmitted to the application’s own
configured agent service — not to an author-chosen host, and no cross-tenant
reads are claimed. The precondition is a deployment that treats workflow
authorship as a lower-trust role than environment/secret configuration —
which is the separation the sensitivity control exists to implement. Under
that model, a workflow author can cause host-scoped values to be sent to the
agent service as conversation identifiers, outside the boundary the guard
family enforces on every adjacent parameter, leaking them into service-side
request handling, logs and telemetry. CVSS 3.1 on that reading is
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N = 7.5.
Microsoft reviewed the report and assessed that the workflow author and the party configuring environment values are the same trust principal, and that the value is only sent to the operator-configured agent service — so no security boundary established for the feature is crossed, and the case was not serviced as a vulnerability. The engineering team nonetheless adopted a fix days later; read this post as a coverage study of a defense-in-depth control, not as a serviced CVE.
The fix
PR #9071 adds a single checked accessor to the executor base class:
protected T GetNonSensitiveValue<T>(EvaluationResult<T> result, string location)
{
if (result.Sensitivity == SensitivityLevel.Sensitive)
{
throw new DeclarativeActionException(
$"Cannot use a sensitive value for {location} in {this.Model.GetType().Name} [{this.Id}].");
}
return result.Value;
}
and routes every reported site through it — all six conversationId
sites and both connectionName sites — plus four additional sites the fix
author identified in the conversation-retrieval executors (conversationId,
messageId, message expression, cursor), with unit tests per executor
(~630 lines of tests). The base-class helper means future executors get the
check by construction instead of by enumeration.
Timeline
- 2026-09-23 → 2026-09-29 — sibling coverage gaps in the same guard family
closed in earlier rounds (HTTP request
#8797, MCP tool#8832, and the invocation-parameter wave). - 2026-09-30 — reported to Microsoft with a differential test harness.
- 2026-10-06 — Microsoft: not serviced as a vulnerability (same trust principal; operator-configured destination). Same day, engineering merged PR #9071 fixing the coverage gap.
- 2026-10-07 — fix released in
Microsoft.Agents.AI.Workflows.Declarative1.24.0 (NuGet); re-verified against the released package.
Disclosure
No advisory or CVE was issued for this report. The public record is the
upstream fix: PR #9071
(commit 0774607d8d). This writeup describes behavior verified against the
repository at both the pre-fix (1a03705a) and fixed (0774607d) revisions,
and re-verified against the published 1.24.0 package.