Description
LabelTrackingFunctionMiddleware hides an untrusted result behind a VariableReferenceContent, which is the right behaviour and the reason the feature is worth having. Hiding is silent, though: a hidden failed compile and a hidden clean compile are the same [var_…] to the model. A tool that knows what its hidden output means has no supported way to say so.
This is a gap 1.19 opened deliberately, and the deliberate part is why it needs a published answer rather than a workaround. Until 1.19 a tool could return one item labelled trusted beside its untrusted output, and _extract_content_label took that item's integrity outright. 1.19 made a per-item label restrict-only — combine_labels(fallback_label, embedded_label) — unless the item carries the framework's own private authoritative marker. That change closes a real hole: a tool could otherwise promote content it produced at call time, so bytes from a web page could be marked trusted and land in the conversation with authority. It also removes the only channel a tool had for a sentence about its own hidden output.
Standing guidance cannot carry what the restriction was defending against, and the difference is structural rather than a matter of degree:
- The text is fixed before the call exists. It comes from the tool definition, so it cannot vary with arguments, with what the body read, or with anything a sandbox or a remote service produced. An attacker who controls every byte the tool touches at runtime controls none of it.
- The framework can be the producer. If the middleware appends the sentences out of
additional_properties, the trusted item never passes through the tool body at all — so this needs no authority grant to a third party and no marker. It is framework-owned content by the same standard quarantined_llm's own stamp meets.
- It is bounded and auditable. A host can read the declared text off every tool it attaches and see, before running anything, the complete set of text that will ever be trusted.
Two mechanisms already in the framework make this a small addition rather than a new idea. SECURITY_TOOL_INSTRUCTIONS is standing text the framework injects to explain what a variable reference is and what to do about it, so the premise — hidden content needs trusted standing text beside it — is already accepted; what is missing is that the block is global, and the useful sentence is per-tool. And apply_mcp_security_labels(..., trust_server_ifc=True) already makes a result label authoritative when local configuration asks for it, under the rule stated beside it: “Local configuration controls result-label authority.” This request needs less than that grants, because the content is fixed at declaration rather than arriving over a wire.
What is being asked for is one entry a tool may declare:
@tool(additional_properties={
"source_integrity": "untrusted",
"standing_guidance": ["A result you cannot read is not a clean validation."],
})
async def validate(files: list[str]) -> str: ...
LabelTrackingFunctionMiddleware appends each sentence to that tool's result as its own Content, labelled trusted at the tool's own confidentiality, after processing the items the body returned. The body never returns them and cannot alter them. A host opt-in alongside it would be reasonable and is not required by the argument above.
Scope. This affects any host that installs LabelTrackingFunctionMiddleware and any tool declaring source_integrity="untrusted" — which is the conservative declaration such a tool should be making. The labels ride in additional_properties and nothing else reads them, so the loss lands exactly on the deployment that turned the security feature on.
What integrators do instead today, none of it good: stamp the private _INTERNAL_RESULT_MARKER, which is a third party impersonating a framework-owned producer, widens the item's reach through allow_principals=True on the same parse, and rests on a name that changed between 1.18 and 1.19; declare the whole tool trusted and label every derived item untrusted, which works on both cores and inverts the direction of failure, since a label the framework cannot parse then falls through to a trusted declaration; or move the sentence into the tool description, which is safe and gives up adjacency to the result it is about.
Code Sample
import asyncio
from agent_framework import Content, FunctionInvocationContext, FunctionTool
from agent_framework.security import LabelTrackingFunctionMiddleware
GUIDANCE = "A result you cannot read is not a clean validation."
async def validate(files: list[str]) -> list[Content]:
return [
Content.from_text("compiler output the model must not act on"),
Content.from_text(
GUIDANCE,
additional_properties={
"security_label": {"integrity": "trusted", "confidentiality": "public"}
},
),
]
tool = FunctionTool(
name="validate", func=validate, additional_properties={"source_integrity": "untrusted"}
)
middleware = LabelTrackingFunctionMiddleware()
context = FunctionInvocationContext(function=tool, arguments={"files": ["main.bicep"]})
async def call_next() -> None:
context.result = await tool.invoke(arguments=context.arguments)
asyncio.run(middleware.process(context, call_next))
for item in context.result:
hidden = (item.additional_properties or {}).get("_variable_reference")
print("hidden" if hidden else repr(item.text))
# agent-framework-core 1.18.0
hidden
'A result you cannot read is not a clean validation.'
# agent-framework-core 1.19.0
hidden
hidden
Language/SDK
Both
Description
LabelTrackingFunctionMiddlewarehides an untrusted result behind aVariableReferenceContent, which is the right behaviour and the reason the feature is worth having. Hiding is silent, though: a hidden failed compile and a hidden clean compile are the same[var_…]to the model. A tool that knows what its hidden output means has no supported way to say so.This is a gap 1.19 opened deliberately, and the deliberate part is why it needs a published answer rather than a workaround. Until 1.19 a tool could return one item labelled
trustedbeside its untrusted output, and_extract_content_labeltook that item's integrity outright. 1.19 made a per-item label restrict-only —combine_labels(fallback_label, embedded_label)— unless the item carries the framework's own private authoritative marker. That change closes a real hole: a tool could otherwise promote content it produced at call time, so bytes from a web page could be marked trusted and land in the conversation with authority. It also removes the only channel a tool had for a sentence about its own hidden output.Standing guidance cannot carry what the restriction was defending against, and the difference is structural rather than a matter of degree:
additional_properties, the trusted item never passes through the tool body at all — so this needs no authority grant to a third party and no marker. It is framework-owned content by the same standardquarantined_llm's own stamp meets.Two mechanisms already in the framework make this a small addition rather than a new idea.
SECURITY_TOOL_INSTRUCTIONSis standing text the framework injects to explain what a variable reference is and what to do about it, so the premise — hidden content needs trusted standing text beside it — is already accepted; what is missing is that the block is global, and the useful sentence is per-tool. Andapply_mcp_security_labels(..., trust_server_ifc=True)already makes a result label authoritative when local configuration asks for it, under the rule stated beside it: “Local configuration controls result-label authority.” This request needs less than that grants, because the content is fixed at declaration rather than arriving over a wire.What is being asked for is one entry a tool may declare:
LabelTrackingFunctionMiddlewareappends each sentence to that tool's result as its ownContent, labelledtrustedat the tool's own confidentiality, after processing the items the body returned. The body never returns them and cannot alter them. A host opt-in alongside it would be reasonable and is not required by the argument above.Scope. This affects any host that installs
LabelTrackingFunctionMiddlewareand any tool declaringsource_integrity="untrusted"— which is the conservative declaration such a tool should be making. The labels ride inadditional_propertiesand nothing else reads them, so the loss lands exactly on the deployment that turned the security feature on.What integrators do instead today, none of it good: stamp the private
_INTERNAL_RESULT_MARKER, which is a third party impersonating a framework-owned producer, widens the item's reach throughallow_principals=Trueon the same parse, and rests on a name that changed between 1.18 and 1.19; declare the whole tool trusted and label every derived item untrusted, which works on both cores and inverts the direction of failure, since a label the framework cannot parse then falls through to a trusted declaration; or move the sentence into the tool description, which is safe and gives up adjacency to the result it is about.Code Sample
Language/SDK
Both